diff --git a/crates/ruff_python_formatter/src/other/parameter.rs b/crates/ruff_python_formatter/src/other/parameter.rs index fb0e84fbcd46d..cac2b5d366a45 100644 --- a/crates/ruff_python_formatter/src/other/parameter.rs +++ b/crates/ruff_python_formatter/src/other/parameter.rs @@ -2,6 +2,7 @@ use ruff_formatter::write; use ruff_python_ast::Parameter; use crate::prelude::*; +use crate::statement::stmt_ann_assign::FormatAnnotation; #[derive(Default)] pub struct FormatParameter; @@ -17,7 +18,7 @@ impl FormatNodeRule for FormatParameter { name.format().fmt(f)?; if let Some(annotation) = annotation { - write!(f, [token(":"), space(), annotation.format()])?; + write!(f, [token(":"), space(), FormatAnnotation::new(annotation)])?; } Ok(()) diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 90379d1ad8fab..52685ddbb43c3 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -24,3 +24,9 @@ pub(crate) const fn is_prefer_splitting_right_hand_side_of_assignments_enabled( ) -> bool { context.is_preview() } + +/// FIXME ADD proper URl once I have internet again. +/// Returns `true` if the [`parenthesize_long_type_hints`](TOOD) preview style is enabled. +pub(crate) const fn is_parenthesize_long_type_hints_enabled(context: &PyFormatContext) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index 89a97acc6f454..73d373bfef818 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -1,10 +1,15 @@ +use crate::builders::parenthesize_if_expands; use ruff_formatter::write; -use ruff_python_ast::StmtAnnAssign; +use ruff_python_ast::{Expr, StmtAnnAssign}; use crate::comments::{SourceComment, SuppressionKind}; -use crate::expression::has_parentheses; +use crate::expression::parentheses::Parentheses; +use crate::expression::{has_own_parentheses, has_parentheses}; use crate::prelude::*; -use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled; +use crate::preview::{ + is_parenthesize_long_type_hints_enabled, + is_prefer_splitting_right_hand_side_of_assignments_enabled, +}; use crate::statement::stmt_assign::{ AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression, }; @@ -27,7 +32,9 @@ impl FormatNodeRule for FormatStmtAnnAssign { if let Some(value) = value { if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) - && has_parentheses(annotation, f.context()).is_some() + && (has_parentheses(annotation, f.context()).is_some() + || (is_parenthesize_long_type_hints_enabled(f.context())) + && should_parenthesize_annotation(annotation, f.context())) { FormatStatementsLastExpression::RightToLeft { before_operator: AnyBeforeOperator::Expression(annotation), @@ -49,7 +56,7 @@ impl FormatNodeRule for FormatStmtAnnAssign { )?; } } else { - annotation.format().fmt(f)?; + FormatAnnotation::new(annotation).fmt(f)?; } if f.options().source_type().is_ipynb() @@ -71,3 +78,45 @@ impl FormatNodeRule for FormatStmtAnnAssign { SuppressionKind::has_skip_comment(trailing_comments, context.source()) } } + +pub(crate) struct FormatAnnotation<'a> { + annotation: &'a Expr, +} + +impl<'a> FormatAnnotation<'a> { + pub(crate) fn new(annotation: &'a Expr) -> Self { + Self { annotation } + } +} + +impl Format> for FormatAnnotation<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if is_parenthesize_long_type_hints_enabled(f.context()) { + let comments = f.context().comments(); + if comments.has_leading(self.annotation) || comments.has_trailing(self.annotation) { + self.annotation + .format() + .with_options(Parentheses::Always) + .fmt(f) + } else if should_parenthesize_annotation(self.annotation, f.context()) { + parenthesize_if_expands(&self.annotation.format().with_options(Parentheses::Never)) + .fmt(f) + } else { + self.annotation + .format() + .with_options(Parentheses::Never) + .fmt(f) + } + } else { + self.annotation.format().fmt(f) + } + } +} + +/// Returns `true` if an annotation should be parenthesized if it splits over multiple lines. +pub(crate) fn should_parenthesize_annotation( + annotation: &Expr, + context: &PyFormatContext<'_>, +) -> bool { + !matches!(annotation, Expr::Name(_)) && has_own_parentheses(annotation, context).is_none() +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap index bf6f676be094e..feeea8206a882 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap @@ -95,62 +95,7 @@ def f( ```diff --- Black +++ Ruff -@@ -7,23 +7,13 @@ - ) - - # "AnnAssign"s now also work --z: ( -- Loooooooooooooooooooooooong -- | Loooooooooooooooooooooooong -- | Loooooooooooooooooooooooong -- | Loooooooooooooooooooooooong --) --z: Short | Short2 | Short3 | Short4 --z: int --z: int -+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong -+z: (Short | Short2 | Short3 | Short4) -+z: (int) -+z: (int) - - --z: ( -- Loooooooooooooooooooooooong -- | Loooooooooooooooooooooooong -- | Loooooooooooooooooooooooong -- | Loooooooooooooooooooooooong --) = 7 -+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 - z: Short | Short2 | Short3 | Short4 = 8 - z: int = 2.3 - z: int = foo() -@@ -63,7 +53,7 @@ - - - # 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. -@@ -72,12 +62,10 @@ - - def foo( - i: int, -- x: ( -- Loooooooooooooooooooooooong -- | Looooooooooooooooong -- | Looooooooooooooooooooong -- | Looooooong -- ), -+ x: Loooooooooooooooooooooooong -+ | Looooooooooooooooong -+ | Looooooooooooooooooooong -+ | Looooooong, - *, - s: str, - ) -> None: -@@ -88,7 +76,7 @@ +@@ -88,7 +88,7 @@ async def foo( q: str | None = Query( None, title="Some long title", description="Some long description" @@ -173,13 +118,23 @@ z = ( ) # "AnnAssign"s now also work -z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong -z: (Short | Short2 | Short3 | Short4) -z: (int) -z: (int) +z: ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) +z: Short | Short2 | Short3 | Short4 +z: int +z: int -z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 +z: ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) = 7 z: Short | Short2 | Short3 | Short4 = 8 z: int = 2.3 z: int = foo() @@ -219,7 +174,7 @@ x = ( # 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. @@ -228,10 +183,12 @@ def foo(i: (int,)) -> None: ... def foo( i: int, - x: Loooooooooooooooooooooooong - | Looooooooooooooooong - | Looooooooooooooooooooong - | Looooooong, + x: ( + Loooooooooooooooooooooooong + | Looooooooooooooooong + | Looooooooooooooooooooong + | Looooooong + ), *, s: str, ) -> None: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__type_annotations.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__type_annotations.py.snap deleted file mode 100644 index 8b8220f9c47cb..0000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__type_annotations.py.snap +++ /dev/null @@ -1,115 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_long_strings__type_annotations.py ---- -## Input - -```python -def func( - arg1, - arg2, -) -> Set["this_is_a_very_long_module_name.AndAVeryLongClasName" - ".WithAVeryVeryVeryVeryVeryLongSubClassName"]: - pass - - -def func( - argument: ( - "VeryLongClassNameWithAwkwardGenericSubtype[int] |" - "VeryLongClassNameWithAwkwardGenericSubtype[str]" - ), -) -> ( - "VeryLongClassNameWithAwkwardGenericSubtype[int] |" - "VeryLongClassNameWithAwkwardGenericSubtype[str]" -): - pass - - -def func( - argument: ( - "int |" - "str" - ), -) -> Set["int |" - " str"]: - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -21,6 +21,6 @@ - - - def func( -- argument: "int |" "str", -+ argument: ("int |" "str"), - ) -> Set["int |" " str"]: - pass -``` - -## Ruff Output - -```python -def func( - arg1, - arg2, -) -> Set[ - "this_is_a_very_long_module_name.AndAVeryLongClasName" - ".WithAVeryVeryVeryVeryVeryLongSubClassName" -]: - pass - - -def func( - argument: ( - "VeryLongClassNameWithAwkwardGenericSubtype[int] |" - "VeryLongClassNameWithAwkwardGenericSubtype[str]" - ), -) -> ( - "VeryLongClassNameWithAwkwardGenericSubtype[int] |" - "VeryLongClassNameWithAwkwardGenericSubtype[str]" -): - pass - - -def func( - argument: ("int |" "str"), -) -> Set["int |" " str"]: - pass -``` - -## Black Output - -```python -def func( - arg1, - arg2, -) -> Set[ - "this_is_a_very_long_module_name.AndAVeryLongClasName" - ".WithAVeryVeryVeryVeryVeryLongSubClassName" -]: - pass - - -def func( - argument: ( - "VeryLongClassNameWithAwkwardGenericSubtype[int] |" - "VeryLongClassNameWithAwkwardGenericSubtype[str]" - ), -) -> ( - "VeryLongClassNameWithAwkwardGenericSubtype[int] |" - "VeryLongClassNameWithAwkwardGenericSubtype[str]" -): - pass - - -def func( - argument: "int |" "str", -) -> Set["int |" " str"]: - pass -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap index 191b6d141bb42..f89b29a9add25 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap @@ -84,6 +84,16 @@ class DefaultRunner: JSONSerializable: TypeAlias = ( "str | int | float | bool | None | list | tuple | JSONMapping" +@@ -29,6 +29,6 @@ + + # Regression test: Don't forget the parentheses in the annotation when breaking + class DefaultRunner: +- task_runner_cls: TaskRunnerProtocol | typing.Callable[ +- [], typing.Any +- ] = DefaultTaskRunner ++ task_runner_cls: TaskRunnerProtocol | typing.Callable[[], typing.Any] = ( ++ DefaultTaskRunner ++ ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap index 4c040409d7c33..4212b53c296dd 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap @@ -1062,7 +1062,7 @@ def function_with_one_argument_and_a_keyword_separator( def argument_with_long_default( -@@ -75,8 +71,7 @@ +@@ -75,22 +71,21 @@ b=ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + [dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff], h=[], @@ -1072,9 +1072,15 @@ def function_with_one_argument_and_a_keyword_separator( def argument_with_long_type_annotation( -@@ -85,12 +80,10 @@ - | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy - | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3], + a, +- b: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +- | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy +- | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3], ++ b: ( ++ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ++ | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy ++ | zzzzzzzzzzzzzzzzzzz ++ ) = [0, 1, 2, 3], h=[], -): - ... @@ -1087,7 +1093,7 @@ def function_with_one_argument_and_a_keyword_separator( # Type parameter empty line spacing -@@ -99,8 +92,7 @@ +@@ -99,8 +94,7 @@ A, # another B, @@ -1097,7 +1103,7 @@ def function_with_one_argument_and_a_keyword_separator( # Type parameter comments -@@ -159,8 +151,7 @@ +@@ -159,8 +153,7 @@ # Comment @@ -1107,7 +1113,7 @@ def function_with_one_argument_and_a_keyword_separator( # Comment that could be mistaken for a trailing comment of the function declaration when -@@ -192,8 +183,7 @@ +@@ -192,8 +185,7 @@ # Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 def foo( b=3 + 2, # comment @@ -1117,7 +1123,7 @@ def function_with_one_argument_and_a_keyword_separator( # Comments on the slash or the star, both of which don't have a node -@@ -454,8 +444,7 @@ +@@ -454,8 +446,7 @@ def f( # first # second @@ -1127,7 +1133,7 @@ def function_with_one_argument_and_a_keyword_separator( def f( # first -@@ -475,8 +464,7 @@ +@@ -475,8 +466,7 @@ # first b, # second @@ -1137,7 +1143,7 @@ def function_with_one_argument_and_a_keyword_separator( def f( # first -@@ -484,8 +472,7 @@ +@@ -484,8 +474,7 @@ # second b, # third @@ -1147,7 +1153,7 @@ def function_with_one_argument_and_a_keyword_separator( def f( # first -@@ -494,8 +481,7 @@ +@@ -494,8 +483,7 @@ # third b, # fourth @@ -1157,7 +1163,7 @@ def function_with_one_argument_and_a_keyword_separator( def f( # first -@@ -522,17 +508,14 @@ +@@ -522,17 +510,14 @@ a, # third /, # second