From 29a97ed2a68c6fac679d187c96489fb05a6f44e8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 20 Dec 2023 18:13:16 +0800 Subject: [PATCH] Parenthesize long type annotations in annotated assignment statements --- .../ruff/statement/long_type_annotations.py | 157 ++++++ .../src/expression/mod.rs | 74 ++- crates/ruff_python_formatter/src/preview.rs | 5 + .../src/statement/stmt_ann_assign.rs | 48 +- .../src/statement/stmt_assign.rs | 24 +- ...es__pep604_union_types_line_breaks.py.snap | 55 +-- .../format@statement__ann_assign.py.snap | 10 + ...t@statement__long_type_annotations.py.snap | 451 ++++++++++++++++++ 8 files changed, 777 insertions(+), 47 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/long_type_annotations.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__long_type_annotations.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/long_type_annotations.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/long_type_annotations.py new file mode 100644 index 00000000000000..ae3a189b471d79 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/long_type_annotations.py @@ -0,0 +1,157 @@ +x1: A[b] | EventHandler | EventSpec | list[EventHandler | EventSpec] | Other | More | AndMore | None = None + +x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]" + +x6: VeryLongClassNameWithAwkwardGenericSubtype[ + integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer, + VeryLongClassNameWithAwkwardGenericSubtype, + str +] = True + + +x7: CustomTrainingJob | CustomContainerTrainingJob | CustomPythonPackageTrainingJob +x8: ( + None + | datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset +) = None + +x9: None | ( + datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset +) = None + + +x10: ( + aaaaaaaaaaaaaaaaaaaaaaaa[ + bbbbbbbbbbb, + Subscript + | None + | datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset, + ], + bbb[other], +) = None + +x11: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] = None + +x12: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, + ] | Other = None + + +x13: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + +x14: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] = None + +x15: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + +x16: None | Literal[ + "split", + "a bit longer", + "records", + "index", + "table", + "columns", + "values", +] = None + +x17: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] + + +class Test: + safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff + applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes. + string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation + + +########## +# Comments + +leading: ( + # Leading comment + None | dataset.ImageDataset +) + +leading_with_value: ( + # Leading comment + None + | dataset.ImageDataset +) = None + +leading_open_parentheses: ( # Leading comment + None + | dataset.ImageDataset +) + +leading_open_parentheses_with_value: ( # Leading comment + None + | dataset.ImageDataset +) = None + +trailing: ( + None | dataset.ImageDataset # trailing comment +) + +trailing_with_value: ( + None | dataset.ImageDataset # trailing comment +) = None + +trailing_own_line: ( + None | dataset.ImageDataset + # trailing own line +) + +trailing_own_line_with_value: ( + None | dataset.ImageDataset + # trailing own line +) = None + +nested_comment: None | [ + # a list of strings + str +] = None diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 7d858694c04368..0e1c17cd8fcabe 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -529,7 +529,7 @@ impl<'ast> IntoFormat> for Expr { /// /// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) #[allow(clippy::if_same_then_else)] -fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { +pub(crate) fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { let mut visitor = CanOmitOptionalParenthesesVisitor::new(context); visitor.visit_subexpression(expr); @@ -1195,3 +1195,75 @@ impl From for OperatorPrecedence { } } } + +/// Returns `true` if `expr` is an expression that can be split into multiple lines. +/// +/// Returns `false` for expressions that are guaranteed to never split. +pub(crate) fn is_splittable_expression(expr: &Expr, context: &PyFormatContext) -> bool { + match expr { + // Single token expressions. They never have any split points. + Expr::NamedExpr(_) + | Expr::Name(_) + | Expr::NumberLiteral(_) + | Expr::BooleanLiteral(_) + | Expr::NoneLiteral(_) + | Expr::EllipsisLiteral(_) + | Expr::Slice(_) + | Expr::IpyEscapeCommand(_) => false, + + // Expressions that insert split points when parenthesized. + Expr::Compare(_) + | Expr::BinOp(_) + | Expr::BoolOp(_) + | Expr::IfExp(_) + | Expr::GeneratorExp(_) + | Expr::Subscript(_) + | Expr::Await(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::YieldFrom(_) => true, + + // Sequence types can split if they contain at least one element. + Expr::Tuple(tuple) => !tuple.elts.is_empty(), + Expr::Dict(dict) => !dict.values.is_empty(), + Expr::Set(set) => !set.elts.is_empty(), + Expr::List(list) => !list.elts.is_empty(), + + Expr::UnaryOp(unary) => is_splittable_expression(unary.operand.as_ref(), context), + Expr::Yield(ast::ExprYield { value, .. }) => value.is_some(), + + Expr::Call(ast::ExprCall { + arguments, func, .. + }) => { + !arguments.is_empty() + || is_expression_parenthesized( + func.as_ref().into(), + context.comments().ranges(), + context.source(), + ) + } + + // String like literals can expand if they are implicit concatenated. + Expr::FString(fstring) => fstring.value.is_implicit_concatenated(), + Expr::StringLiteral(string) => string.value.is_implicit_concatenated(), + Expr::BytesLiteral(bytes) => bytes.value.is_implicit_concatenated(), + + // Expressions that have no split points per se, but they contain nested sub expressions that might expand. + Expr::Lambda(ast::ExprLambda { + body: expression, .. + }) + | Expr::Starred(ast::ExprStarred { + value: expression, .. + }) + | Expr::Attribute(ast::ExprAttribute { + value: expression, .. + }) => { + is_expression_parenthesized( + (&*expression).into(), + context.comments().ranges(), + context.source(), + ) || is_splittable_expression(expression.as_ref(), context) + } + } +} diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 4de87cf05c9118..bc132691f1266a 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -25,6 +25,11 @@ pub(crate) const fn is_prefer_splitting_right_hand_side_of_assignments_enabled( context.is_preview() } +/// Returns `true` if the [`parenthesize_long_type_hints`](https://github.com/astral-sh/ruff/issues/8894) preview style is enabled. +pub(crate) const fn is_parenthesize_long_type_hints_enabled(context: &PyFormatContext) -> bool { + context.is_preview() +} + /// Returns `true` if the [`no_blank_line_before_class_docstring`] preview style is enabled. /// /// [`no_blank_line_before_class_docstring`]: https://github.com/astral-sh/ruff/issues/8888 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 89a97acc6f454c..5d0a2d2f31eedd 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -2,9 +2,13 @@ use ruff_formatter::write; use ruff_python_ast::StmtAnnAssign; use crate::comments::{SourceComment, SuppressionKind}; -use crate::expression::has_parentheses; +use crate::expression::parentheses::Parentheses; +use crate::expression::{has_parentheses, is_splittable_expression}; 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 +31,11 @@ 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() + // The `has_parentheses` check can be removed when stabilizing `is_parenthesize_long_type_hints`. + // because `is_splittable_expression` covers both. + && (has_parentheses(annotation, f.context()).is_some() + || (is_parenthesize_long_type_hints_enabled(f.context()) + && is_splittable_expression(annotation, f.context()))) { FormatStatementsLastExpression::RightToLeft { before_operator: AnyBeforeOperator::Expression(annotation), @@ -37,10 +45,28 @@ impl FormatNodeRule for FormatStmtAnnAssign { } .fmt(f)?; } else { + // Remove unnecessary parentheses around the annotation if the parenthesize long type hints preview style is enabled. + // Ensure we keep the parentheses if the annotation has any comments. + if is_parenthesize_long_type_hints_enabled(f.context()) { + if f.context().comments().has_leading(annotation.as_ref()) + || f.context().comments().has_trailing(annotation.as_ref()) + { + annotation + .format() + .with_options(Parentheses::Always) + .fmt(f)?; + } else { + annotation + .format() + .with_options(Parentheses::Never) + .fmt(f)?; + } + } else { + annotation.format().fmt(f)?; + } write!( f, [ - annotation.format(), space(), token("="), space(), @@ -49,7 +75,19 @@ impl FormatNodeRule for FormatStmtAnnAssign { )?; } } else { - annotation.format().fmt(f)?; + // Parenthesize the value and inline the comment if it is a "simple" type annotation, similar + // to what we do with the value. + // ```python + // class Test: + // safe_age: ( + // Decimal # the user's age, used to determine if it's safe for them to use ruff + // ) + // ``` + if is_parenthesize_long_type_hints_enabled(f.context()) { + FormatStatementsLastExpression::left_to_right(annotation, item).fmt(f)?; + } else { + annotation.format().fmt(f)?; + } } if f.options().source_type().is_ipynb() diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index d9f077b7c71ee3..b709c1a08340f0 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -9,11 +9,18 @@ use crate::comments::{ }; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{ - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, + is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses, + Parentheses, Parenthesize, +}; +use crate::expression::{ + can_omit_optional_parentheses, has_own_parentheses, has_parentheses, + maybe_parenthesize_expression, }; -use crate::expression::{has_own_parentheses, has_parentheses, maybe_parenthesize_expression}; 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::trailing_semicolon; #[derive(Default)] @@ -686,8 +693,17 @@ impl Format> for AnyBeforeOperator<'_> { } // Never parenthesize targets that come with their own parentheses, e.g. don't parenthesize lists or dictionary literals. else if should_parenthesize_target(expression, f.context()) { - parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + if is_parenthesize_long_type_hints_enabled(f.context()) + && can_omit_optional_parentheses(expression, f.context()) + { + optional_parentheses(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } else { + parenthesize_if_expands( + &expression.format().with_options(Parentheses::Never), + ) .fmt(f) + } } else { expression.format().with_options(Parentheses::Never).fmt(f) } 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 bf6f676be094e3..d84b08e5125c60 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,36 +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 @@ +@@ -63,7 +63,7 @@ # remove unnecessary paren @@ -133,7 +104,7 @@ def f( # 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 @@ +@@ -72,12 +72,10 @@ def foo( i: int, @@ -150,7 +121,7 @@ def f( *, s: str, ) -> None: -@@ -88,7 +76,7 @@ +@@ -88,7 +86,7 @@ async def foo( q: str | None = Query( None, title="Some long title", description="Some long description" @@ -173,13 +144,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() 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 191b6d141bb42a..f89b29a9add253 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__long_type_annotations.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__long_type_annotations.py.snap new file mode 100644 index 00000000000000..184509d8063f18 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__long_type_annotations.py.snap @@ -0,0 +1,451 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/long_type_annotations.py +--- +## Input +```python +x1: A[b] | EventHandler | EventSpec | list[EventHandler | EventSpec] | Other | More | AndMore | None = None + +x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]" + +x6: VeryLongClassNameWithAwkwardGenericSubtype[ + integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer, + VeryLongClassNameWithAwkwardGenericSubtype, + str +] = True + + +x7: CustomTrainingJob | CustomContainerTrainingJob | CustomPythonPackageTrainingJob +x8: ( + None + | datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset +) = None + +x9: None | ( + datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset +) = None + + +x10: ( + aaaaaaaaaaaaaaaaaaaaaaaa[ + bbbbbbbbbbb, + Subscript + | None + | datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset, + ], + bbb[other], +) = None + +x11: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] = None + +x12: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, + ] | Other = None + + +x13: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + +x14: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] = None + +x15: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + +x16: None | Literal[ + "split", + "a bit longer", + "records", + "index", + "table", + "columns", + "values", +] = None + +x17: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] + + +class Test: + safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff + applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes. + string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation + + +########## +# Comments + +leading: ( + # Leading comment + None | dataset.ImageDataset +) + +leading_with_value: ( + # Leading comment + None + | dataset.ImageDataset +) = None + +leading_open_parentheses: ( # Leading comment + None + | dataset.ImageDataset +) + +leading_open_parentheses_with_value: ( # Leading comment + None + | dataset.ImageDataset +) = None + +trailing: ( + None | dataset.ImageDataset # trailing comment +) + +trailing_with_value: ( + None | dataset.ImageDataset # trailing comment +) = None + +trailing_own_line: ( + None | dataset.ImageDataset + # trailing own line +) + +trailing_own_line_with_value: ( + None | dataset.ImageDataset + # trailing own line +) = None + +nested_comment: None | [ + # a list of strings + str +] = None +``` + +## Output +```python +x1: A[b] | EventHandler | EventSpec | list[ + EventHandler | EventSpec +] | Other | More | AndMore | None = None + +x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]" + +x6: VeryLongClassNameWithAwkwardGenericSubtype[ + integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer, + VeryLongClassNameWithAwkwardGenericSubtype, + str, +] = True + + +x7: CustomTrainingJob | CustomContainerTrainingJob | CustomPythonPackageTrainingJob +x8: ( + None + | datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset +) = None + +x9: None | ( + datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset +) = None + + +x10: ( + aaaaaaaaaaaaaaaaaaaaaaaa[ + bbbbbbbbbbb, + Subscript + | None + | datasets.ImageDataset + | datasets.TabularDataset + | datasets.TextDataset + | datasets.VideoDataset, + ], + bbb[other], +) = None + +x11: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] = None + +x12: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + + +x13: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + +x14: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] = None + +x15: [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] | Other = None + +x16: None | Literal[ + "split", + "a bit longer", + "records", + "index", + "table", + "columns", + "values", +] = None + +x17: None | [ + datasets.ImageDataset, + datasets.TabularDataset, + datasets.TextDataset, + datasets.VideoDataset, +] + + +class Test: + safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff + applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes. + string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation + + +########## +# Comments + +leading: ( + # Leading comment + None | dataset.ImageDataset +) + +leading_with_value: ( + # Leading comment + None | dataset.ImageDataset +) = None + +leading_open_parentheses: ( # Leading comment + None | dataset.ImageDataset +) + +leading_open_parentheses_with_value: ( # Leading comment + None | dataset.ImageDataset +) = None + +trailing: ( + None | dataset.ImageDataset # trailing comment +) + +trailing_with_value: ( + None | dataset.ImageDataset # trailing comment +) = None + +trailing_own_line: ( + None | dataset.ImageDataset + # trailing own line +) + +trailing_own_line_with_value: ( + None | dataset.ImageDataset + # trailing own line +) = None + +nested_comment: None | [ + # a list of strings + str +] = None +``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -1,8 +1,18 @@ +-x1: A[b] | EventHandler | EventSpec | list[ +- EventHandler | EventSpec +-] | Other | More | AndMore | None = None ++x1: ( ++ A[b] ++ | EventHandler ++ | EventSpec ++ | list[EventHandler | EventSpec] ++ | Other ++ | More ++ | AndMore ++ | None ++) = None + +-x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]" ++x2: ( ++ "VeryLongClassNameWithAwkwardGenericSubtype[int] |" ++ "VeryLongClassNameWithAwkwardGenericSubtype[str]" ++) + + x6: VeryLongClassNameWithAwkwardGenericSubtype[ + integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer, +@@ -48,12 +58,16 @@ + datasets.VideoDataset, + ] = None + +-x12: None | [ +- datasets.ImageDataset, +- datasets.TabularDataset, +- datasets.TextDataset, +- datasets.VideoDataset, +-] | Other = None ++x12: ( ++ None ++ | [ ++ datasets.ImageDataset, ++ datasets.TabularDataset, ++ datasets.TextDataset, ++ datasets.VideoDataset, ++ ] ++ | Other ++) = None + + + x13: [ +@@ -75,27 +89,34 @@ + datasets.VideoDataset, + ] = None + +-x15: [ +- datasets.ImageDataset, +- datasets.TabularDataset, +- datasets.TextDataset, +- datasets.VideoDataset, +-] | [ +- datasets.ImageDataset, +- datasets.TabularDataset, +- datasets.TextDataset, +- datasets.VideoDataset, +-] | Other = None ++x15: ( ++ [ ++ datasets.ImageDataset, ++ datasets.TabularDataset, ++ datasets.TextDataset, ++ datasets.VideoDataset, ++ ] ++ | [ ++ datasets.ImageDataset, ++ datasets.TabularDataset, ++ datasets.TextDataset, ++ datasets.VideoDataset, ++ ] ++ | Other ++) = None + +-x16: None | Literal[ +- "split", +- "a bit longer", +- "records", +- "index", +- "table", +- "columns", +- "values", +-] = None ++x16: ( ++ None ++ | Literal[ ++ "split", ++ "a bit longer", ++ "records", ++ "index", ++ "table", ++ "columns", ++ "values", ++ ] ++) = None + + x17: None | [ + datasets.ImageDataset, +@@ -106,9 +127,13 @@ + + + class Test: +- safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff ++ safe_age: ( ++ Decimal # the user's age, used to determine if it's safe for them to use ruff ++ ) + applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes. +- string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation ++ string_annotation: ( ++ "Test" # a long comment after a quoted, runtime-only type annotation ++ ) + + + ########## +``` + + +