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..b1c442ecfcda34 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, Parenthesize};
+use crate::expression::{has_parentheses, is_splittable_expression, 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::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..fb63d4cdc0ce64 100644
--- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs
+++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs
@@ -1,4 +1,4 @@
-use ruff_formatter::{format_args, write, FormatError};
+use ruff_formatter::{format_args, write, Arguments, FormatError, GroupId};
use ruff_python_ast::{
AnyNodeRef, Expr, ExprAttribute, ExprCall, Operator, StmtAssign, TypeParams,
};
@@ -9,9 +9,13 @@ 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::statement::trailing_semicolon;
@@ -686,8 +690,15 @@ 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 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
++ )
+
+
+ ##########
+```
+
+
+
diff --git a/output.md b/output.md
new file mode 100644
index 00000000000000..8873dad9cfd41f
--- /dev/null
+++ b/output.md
@@ -0,0 +1,17 @@
+ℹ️ ecosystem check **encountered format errors**. (no format changes; 1 project error)
+
+pypa/pip (error)
+
+
ruff format --preview
+
+
+
+```
+Failed to pull: error: 2002 bytes of body are still expected
+fetch-pack: unexpected disconnect while reading sideband packet
+fatal: early EOF
+fatal: fetch-pack: invalid index-pack output
+```
+
+
+