From 1a60d1e3c6c07e22874d5e2293c7737e7a31e757 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 2 Aug 2023 15:29:28 -0500 Subject: [PATCH] Add formatting of type parameters in class and function definitions (#6161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #5062 Closes https://github.com/astral-sh/ruff/issues/5931 Implements formatting of a sequence of type parameters in a dedicated struct for reuse by classes, functions, and type aliases (preparing for #5929). Adds formatting of type parameters in class and function definitions — previously, they were just elided. --- crates/ruff_python_ast/src/function.rs | 8 + crates/ruff_python_ast/src/node.rs | 38 +++++ crates/ruff_python_formatter/generate.py | 1 + .../test/fixtures/ruff/expression/list.py | 11 ++ .../ruff/statement/class_definition.py | 62 +++++++ .../test/fixtures/ruff/statement/function.py | 46 +++++ .../src/comments/placement.rs | 48 +++++- .../src/comments/visitor.rs | 9 +- crates/ruff_python_formatter/src/generated.rs | 160 ++++++++++++++++++ crates/ruff_python_formatter/src/lib.rs | 1 + .../src/statement/stmt_class_def.rs | 9 +- .../src/statement/stmt_function_def.rs | 16 +- .../src/type_param/mod.rs | 37 ++++ .../src/type_param/type_param_clause.rs | 29 ++++ .../src/type_param/type_param_param_spec.rs | 14 ++ .../src/type_param/type_param_type_var.rs | 22 +++ .../type_param/type_param_type_var_tuple.rs | 14 ++ .../src/type_param/type_params.rs | 41 +++++ ..._compatibility@py_312__type_params.py.snap | 159 ----------------- .../snapshots/format@expression__list.py.snap | 21 +++ ...format@statement__class_definition.py.snap | 141 +++++++++++++++ .../format@statement__function.py.snap | 110 ++++++++++++ 22 files changed, 825 insertions(+), 172 deletions(-) create mode 100644 crates/ruff_python_formatter/src/type_param/mod.rs create mode 100644 crates/ruff_python_formatter/src/type_param/type_param_clause.rs create mode 100644 crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs create mode 100644 crates/ruff_python_formatter/src/type_param/type_param_type_var.rs create mode 100644 crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs create mode 100644 crates/ruff_python_formatter/src/type_param/type_params.rs delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_312__type_params.py.snap diff --git a/crates/ruff_python_ast/src/function.rs b/crates/ruff_python_ast/src/function.rs index 8d1c290118a3a..96737bcf8641a 100644 --- a/crates/ruff_python_ast/src/function.rs +++ b/crates/ruff_python_ast/src/function.rs @@ -1,6 +1,7 @@ use crate::node::AnyNodeRef; use crate::{ Decorator, Expr, Identifier, Parameters, Ranged, StmtAsyncFunctionDef, StmtFunctionDef, Suite, + TypeParams, }; use ruff_text_size::TextRange; @@ -79,6 +80,13 @@ impl<'a> AnyFunctionDefinition<'a> { } } + pub fn type_params(self) -> Option<&'a TypeParams> { + match self { + Self::FunctionDefinition(definition) => definition.type_params.as_ref(), + Self::AsyncFunctionDefinition(definition) => definition.type_params.as_ref(), + } + } + /// Returns `true` if this is [`Self::AsyncFunctionDefinition`] pub const fn is_async(self) -> bool { matches!(self, Self::AsyncFunctionDefinition(_)) diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index a9462cf48f36f..6f7f2c911f04a 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -2880,6 +2880,34 @@ impl AstNode for Decorator { AnyNode::from(self) } } +impl AstNode for ast::TypeParams { + fn cast(kind: AnyNode) -> Option + where + Self: Sized, + { + if let AnyNode::TypeParams(node) = kind { + Some(node) + } else { + None + } + } + + fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { + if let AnyNodeRef::TypeParams(node) = kind { + Some(node) + } else { + None + } + } + + fn as_any_node_ref(&self) -> AnyNodeRef { + AnyNodeRef::from(self) + } + + fn into_any_node(self) -> AnyNode { + AnyNode::from(self) + } +} impl AstNode for ast::TypeParamTypeVar { fn cast(kind: AnyNode) -> Option where @@ -3531,6 +3559,11 @@ impl From for AnyNode { AnyNode::Decorator(node) } } +impl From for AnyNode { + fn from(node: TypeParams) -> Self { + AnyNode::TypeParams(node) + } +} impl From for AnyNode { fn from(node: TypeParamTypeVar) -> Self { AnyNode::TypeParamTypeVar(node) @@ -4804,6 +4837,11 @@ impl<'a> From<&'a Decorator> for AnyNodeRef<'a> { } } +impl<'a> From<&'a ast::TypeParams> for AnyNodeRef<'a> { + fn from(node: &'a ast::TypeParams) -> Self { + AnyNodeRef::TypeParams(node) + } +} impl<'a> From<&'a TypeParamTypeVar> for AnyNodeRef<'a> { fn from(node: &'a TypeParamTypeVar) -> Self { AnyNodeRef::TypeParamTypeVar(node) diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index ba52df6e969cf..3acc858efd9f4 100755 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -54,6 +54,7 @@ def rustfmt(code: str) -> str: "expr": "expression", "stmt": "statement", "pattern": "pattern", + "type_param": "type_param", "other": "other", } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py index f0fedc6957da1..209cc0be84fab 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py @@ -21,3 +21,14 @@ aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa ] + +# Comment placement in non-empty lists +c1 = [ # trailing open bracket + # leading item + 1, + + # between + + 2, # trailing item + # leading close bracket +] # trailing close bracket diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py index 829457fdb19b3..442441465e941 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py @@ -43,6 +43,14 @@ class TestTrailingComment2: # trailing comment pass +class TestTrailingComment3[T]: # trailing comment + pass + + +class TestTrailingComment4[T](A): # trailing comment + pass + + class Test: """Docstring""" @@ -144,3 +152,57 @@ class AltCLIPOutput( # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP ): ... + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, Cccccccccccccccccccccc]: + pass + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, *Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, **Cccccccccccccccccccccc]: + pass + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: + pass + + +class TestTypeParams[*Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: + pass + + +class TestTypeParams[**Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: + pass + + +class TestTypeParams[**P, *Ts, T]: + pass + + +class TestTypeParams[ # trailing bracket comment + # leading comment + A, + + # in between comment + + B, + # another leading comment + C, + D, # trailing comment + # leading bracket comment +]: + pass + + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](B, C, D): + pass + + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, Cccccccccccccccccccccccc, Ddddddddddddddddddddddddd): + pass + + + +class TestTypeParams[A, B, C](meta=Aaaaaaaaaaaaaaaaaaaaaa): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py index 9a2529209a6c3..95573170b12b7 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py @@ -66,6 +66,52 @@ def argument_with_long_type_annotation( def test(): ... +# Type parameter empty line spacing +def test[ + # comment + A, + + # another + + B, +](): ... + + +# Type parameter comments +def type_param_comments[ # trailing bracket comment + # leading comment + A, + + # in between comment + + B, + # another leading comment + C, + D, # trailing comment + # leading bracket comment +](): + # body comment + pass + + +# Note empty type parameters is not valid syntax, e.g. +# def test[](): ... + + +# Different type parameter wrappings + +def single_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccccccccc](): + pass + +def params_on_their_own_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccc, Ddddddddddddd, Eeeeeeee](): + pass + +def param_per_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccccccccc, Ddddddddddddd, Eeeeeeeeeeeeeeeee, ffffffffffff](): + pass + +def single_line_trailing_comma[A, B, C,](): + pass + # Comment def with_leading_comment(): ... diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 09ea37a0ee5a1..ddd0080cc36d2 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -4,7 +4,7 @@ use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{ self as ast, Arguments, Comprehension, Expr, ExprAttribute, ExprBinOp, ExprIfExp, ExprSlice, - ExprStarred, MatchCase, Parameters, Ranged, + ExprStarred, MatchCase, Parameters, Ranged, TypeParams, }; use ruff_python_trivia::{ indentation_at_offset, PythonWhitespace, SimpleToken, SimpleTokenKind, SimpleTokenizer, @@ -85,6 +85,7 @@ pub(super) fn place_comment<'a>( handle_leading_class_with_decorators_comment(comment, class_def) } AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from), + AnyNodeRef::TypeParams(type_params) => handle_type_params_comment(comment, type_params), _ => CommentPlacement::Default(comment), } } @@ -563,6 +564,51 @@ fn handle_own_line_comment_after_branch<'a>( } } +/// Attach an enclosed end-of-line comment to a set of [`TypeParams`]. +/// +/// For example, given: +/// ```python +/// type foo[ # comment +/// bar, +/// ] = ... +/// ``` +/// +/// The comment will be attached to the [`TypeParams`] node as a dangling comment, to ensure +/// that it remains on the same line as open bracket. +fn handle_type_params_comment<'a>( + comment: DecoratedComment<'a>, + type_params: &'a TypeParams, +) -> CommentPlacement<'a> { + // The comment needs to be on the same line, but before the first type param. For example, we want + // to treat this as a dangling comment: + // ```python + // type foo[ # comment + // bar, + // baz, + // qux, + // ] + // ``` + // However, this should _not_ be treated as a dangling comment: + // ```python + // type foo[bar, # comment + // baz, + // qux, + // ] = ... + // ``` + // Thus, we check whether the comment is an end-of-line comment _between_ the start of the + // statement and the first type param. If so, the only possible position is immediately following + // the open parenthesis. + if comment.line_position().is_end_of_line() { + if let Some(first_type_param) = type_params.type_params.first() { + if type_params.start() < comment.start() && comment.end() < first_type_param.start() { + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + } + } + + CommentPlacement::Default(comment) +} + /// Attaches comments for the positional-only parameters separator `/` or the keywords-only /// parameters separator `*` as dangling comments to the enclosing [`Parameters`] node. /// diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index ed664a95569d6..8f2d61f016df8 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -3,7 +3,7 @@ use std::iter::Peekable; use ruff_python_ast::{ Alias, Arguments, Comprehension, Decorator, ElifElseClause, ExceptHandler, Expr, Keyword, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt, TypeParam, - WithItem, + TypeParams, WithItem, }; use ruff_text_size::{TextRange, TextSize}; @@ -301,6 +301,13 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(elif_else_clause); } + fn visit_type_params(&mut self, type_params: &'ast TypeParams) { + if self.start_node(type_params).is_traverse() { + walk_type_params(self, type_params); + } + self.finish_node(type_params); + } + fn visit_type_param(&mut self, type_param: &'ast TypeParam) { if self.start_node(type_param).is_traverse() { walk_type_param(self, type_param); diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index e6346442f4a9a..d50efff83c4b9 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -2939,3 +2939,163 @@ impl<'ast> IntoFormat> for ast::ElifElseClause { ) } } + +impl FormatRule> + for crate::type_param::type_params::FormatTypeParams +{ + #[inline] + fn fmt( + &self, + node: &ast::TypeParams, + f: &mut Formatter>, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::TypeParams { + type Format<'a> = FormatRefWithRule< + 'a, + ast::TypeParams, + crate::type_param::type_params::FormatTypeParams, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::type_param::type_params::FormatTypeParams::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::TypeParams { + type Format = FormatOwnedWithRule< + ast::TypeParams, + crate::type_param::type_params::FormatTypeParams, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::type_param::type_params::FormatTypeParams::default(), + ) + } +} + +impl FormatRule> + for crate::type_param::type_param_type_var::FormatTypeParamTypeVar +{ + #[inline] + fn fmt( + &self, + node: &ast::TypeParamTypeVar, + f: &mut Formatter>, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::TypeParamTypeVar { + type Format<'a> = FormatRefWithRule< + 'a, + ast::TypeParamTypeVar, + crate::type_param::type_param_type_var::FormatTypeParamTypeVar, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::type_param::type_param_type_var::FormatTypeParamTypeVar::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::TypeParamTypeVar { + type Format = FormatOwnedWithRule< + ast::TypeParamTypeVar, + crate::type_param::type_param_type_var::FormatTypeParamTypeVar, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::type_param::type_param_type_var::FormatTypeParamTypeVar::default(), + ) + } +} + +impl FormatRule> + for crate::type_param::type_param_type_var_tuple::FormatTypeParamTypeVarTuple +{ + #[inline] + fn fmt( + &self, + node: &ast::TypeParamTypeVarTuple, + f: &mut Formatter>, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::TypeParamTypeVarTuple { + type Format<'a> = FormatRefWithRule< + 'a, + ast::TypeParamTypeVarTuple, + crate::type_param::type_param_type_var_tuple::FormatTypeParamTypeVarTuple, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::type_param::type_param_type_var_tuple::FormatTypeParamTypeVarTuple::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::TypeParamTypeVarTuple { + type Format = FormatOwnedWithRule< + ast::TypeParamTypeVarTuple, + crate::type_param::type_param_type_var_tuple::FormatTypeParamTypeVarTuple, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::type_param::type_param_type_var_tuple::FormatTypeParamTypeVarTuple::default(), + ) + } +} + +impl FormatRule> + for crate::type_param::type_param_param_spec::FormatTypeParamParamSpec +{ + #[inline] + fn fmt( + &self, + node: &ast::TypeParamParamSpec, + f: &mut Formatter>, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::TypeParamParamSpec { + type Format<'a> = FormatRefWithRule< + 'a, + ast::TypeParamParamSpec, + crate::type_param::type_param_param_spec::FormatTypeParamParamSpec, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::type_param::type_param_param_spec::FormatTypeParamParamSpec::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::TypeParamParamSpec { + type Format = FormatOwnedWithRule< + ast::TypeParamParamSpec, + crate::type_param::type_param_param_spec::FormatTypeParamParamSpec, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::type_param::type_param_param_spec::FormatTypeParamParamSpec::default(), + ) + } +} diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 89bf5493857e3..7813c5d485e48 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -34,6 +34,7 @@ pub(crate) mod other; pub(crate) mod pattern; mod prelude; pub(crate) mod statement; +pub(crate) mod type_param; include!("../../ruff_formatter/shared_traits.rs"); diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 3333fa91e7cb7..62579dc27f372 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,10 +1,11 @@ -use ruff_formatter::write; +use ruff_formatter::{write, Buffer}; use ruff_python_ast::{Ranged, StmtClassDef}; use ruff_python_trivia::{lines_after, skip_trailing_trivia}; use crate::comments::{leading_comments, trailing_comments}; use crate::prelude::*; use crate::statement::suite::SuiteKind; +use crate::FormatNodeRule; #[derive(Default)] pub struct FormatStmtClassDef; @@ -16,7 +17,7 @@ impl FormatNodeRule for FormatStmtClassDef { name, arguments, body, - type_params: _, + type_params, decorator_list, } = item; @@ -58,6 +59,10 @@ impl FormatNodeRule for FormatStmtClassDef { write!(f, [text("class"), space(), name.format()])?; + if let Some(type_params) = type_params.as_deref() { + write!(f, [type_params.format()])?; + } + if let Some(arguments) = arguments.as_deref() { // Drop empty parentheses, e.g., in: // ```python diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 7290a9ca853a0..4580a156663ae 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -80,15 +80,13 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun let name = item.name(); - write!( - f, - [ - text("def"), - space(), - name.format(), - item.arguments().format(), - ] - )?; + write!(f, [text("def"), space(), name.format()])?; + + if let Some(type_params) = item.type_params() { + write!(f, [type_params.format()])?; + } + + write!(f, [item.arguments().format()])?; if let Some(return_annotation) = item.returns() { write!( diff --git a/crates/ruff_python_formatter/src/type_param/mod.rs b/crates/ruff_python_formatter/src/type_param/mod.rs new file mode 100644 index 0000000000000..07c5f0349f88e --- /dev/null +++ b/crates/ruff_python_formatter/src/type_param/mod.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; +use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule}; +use ruff_python_ast::TypeParam; + +pub(crate) mod type_param_param_spec; +pub(crate) mod type_param_type_var; +pub(crate) mod type_param_type_var_tuple; +pub(crate) mod type_params; + +#[derive(Default)] +pub struct FormatTypeParam; + +impl FormatRule> for FormatTypeParam { + fn fmt(&self, item: &TypeParam, f: &mut PyFormatter) -> FormatResult<()> { + match item { + TypeParam::TypeVar(x) => x.format().fmt(f), + TypeParam::TypeVarTuple(x) => x.format().fmt(f), + TypeParam::ParamSpec(x) => x.format().fmt(f), + } + } +} + +impl<'ast> AsFormat> for TypeParam { + type Format<'a> = FormatRefWithRule<'a, TypeParam, FormatTypeParam, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatTypeParam) + } +} + +impl<'ast> IntoFormat> for TypeParam { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatTypeParam) + } +} diff --git a/crates/ruff_python_formatter/src/type_param/type_param_clause.rs b/crates/ruff_python_formatter/src/type_param/type_param_clause.rs new file mode 100644 index 0000000000000..f311813d5ba9b --- /dev/null +++ b/crates/ruff_python_formatter/src/type_param/type_param_clause.rs @@ -0,0 +1,29 @@ +use crate::builders::PyFormatterExtensions; +use crate::context::PyFormatContext; +use crate::expression::parentheses::parenthesized; +use crate::prelude::*; +use ruff_formatter::{Format, FormatResult}; +use ruff_python_ast::TypeParam; +use ruff_text_size::TextSize; + +pub(crate) struct FormatTypeParamsClause<'a> { + pub(crate) sequence_end: TextSize, + pub(crate) type_params: &'a Vec, +} + +/// Formats a sequence of [`TypeParam`] nodes. +impl Format> for FormatTypeParamsClause<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if self.type_params.is_empty() { + return Ok(()); + } + + let items = format_with(|f| { + f.join_comma_separated(self.sequence_end) + .nodes(self.type_params.iter()) + .finish() + }); + + parenthesized("[", &items, "]").fmt(f) + } +} diff --git a/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs b/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs new file mode 100644 index 0000000000000..ad99dfc4c5088 --- /dev/null +++ b/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs @@ -0,0 +1,14 @@ +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::text; +use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::TypeParamParamSpec; + +#[derive(Default)] +pub struct FormatTypeParamParamSpec; + +impl FormatNodeRule for FormatTypeParamParamSpec { + fn fmt_fields(&self, item: &TypeParamParamSpec, f: &mut PyFormatter) -> FormatResult<()> { + let TypeParamParamSpec { range: _, name } = item; + write!(f, [text("**"), name.format()]) + } +} diff --git a/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs b/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs new file mode 100644 index 0000000000000..c6f9812cbb339 --- /dev/null +++ b/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs @@ -0,0 +1,22 @@ +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; +use ruff_python_ast::TypeParamTypeVar; + +#[derive(Default)] +pub struct FormatTypeParamTypeVar; + +impl FormatNodeRule for FormatTypeParamTypeVar { + fn fmt_fields(&self, item: &TypeParamTypeVar, f: &mut PyFormatter) -> FormatResult<()> { + let TypeParamTypeVar { + range: _, + name, + bound, + } = item; + name.format().fmt(f)?; + if let Some(bound) = bound { + write!(f, [text(":"), space(), bound.format()])?; + } + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs b/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs new file mode 100644 index 0000000000000..d9e6640822ef6 --- /dev/null +++ b/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs @@ -0,0 +1,14 @@ +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::text; +use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::TypeParamTypeVarTuple; + +#[derive(Default)] +pub struct FormatTypeParamTypeVarTuple; + +impl FormatNodeRule for FormatTypeParamTypeVarTuple { + fn fmt_fields(&self, item: &TypeParamTypeVarTuple, f: &mut PyFormatter) -> FormatResult<()> { + let TypeParamTypeVarTuple { range: _, name } = item; + write!(f, [text("*"), name.format()]) + } +} diff --git a/crates/ruff_python_formatter/src/type_param/type_params.rs b/crates/ruff_python_formatter/src/type_param/type_params.rs new file mode 100644 index 0000000000000..8c82caec1e30f --- /dev/null +++ b/crates/ruff_python_formatter/src/type_param/type_params.rs @@ -0,0 +1,41 @@ +use crate::builders::PyFormatterExtensions; +use crate::comments::trailing_comments; +use crate::expression::parentheses::parenthesized; +use crate::prelude::*; +use ruff_formatter::write; +use ruff_formatter::FormatResult; +use ruff_python_ast::node::AstNode; + +use ruff_python_ast::TypeParams; + +#[derive(Default)] +pub struct FormatTypeParams; + +/// Formats a sequence of [`TypeParam`] nodes. +impl FormatNodeRule for FormatTypeParams { + fn fmt_fields(&self, item: &TypeParams, f: &mut PyFormatter) -> FormatResult<()> { + // A dangling comment indicates a comment on the same line as the opening bracket, e.g.: + // ```python + // type foo[ # This type parameter clause has a dangling comment. + // a, + // b, + // c, + // ] = ... + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + write!(f, [trailing_comments(dangling_comments)])?; + + let items = format_with(|f| { + f.join_comma_separated(item.range.end()) + .nodes(item.type_params.iter()) + .finish() + }); + + parenthesized("[", &items, "]").fmt(f) + } + + fn fmt_dangling_comments(&self, _node: &TypeParams, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_312__type_params.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_312__type_params.py.snap deleted file mode 100644 index c49fd8fb31f31..0000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_312__type_params.py.snap +++ /dev/null @@ -1,159 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_312/type_params.py ---- -## Input - -```py -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 -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,40 +1,30 @@ --def func[T](): -+def func(): - pass - - --async def func[T](): -+async def func(): - pass - - --class C[T]: -+class C: - pass - - --def all_in[T: int, U: (bytes, str), *Ts, **P](): -+def all_in(): - pass - - --def really_long[ -- WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine --](): -+def really_long(): - pass - - --def even_longer[ -- WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound --](): -+def even_longer(): - pass - - --def it_gets_worse[ -- WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine, -- ItCouldBeGenericOverMultipleTypeVars, --](): -+def it_gets_worse(): - pass - - --def magic[ -- Trailing, -- Comma, --](): -+def magic(): - pass -``` - -## Ruff Output - -```py -def func(): - pass - - -async def func(): - pass - - -class C: - pass - - -def all_in(): - pass - - -def really_long(): - pass - - -def even_longer(): - pass - - -def it_gets_worse(): - pass - - -def magic(): - pass -``` - -## Black Output - -```py -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 -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap index f84edaf59288b..5d72b05ea7437 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap @@ -27,6 +27,17 @@ b3 = [ aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa ] + +# Comment placement in non-empty lists +c1 = [ # trailing open bracket + # leading item + 1, + + # between + + 2, # trailing item + # leading close bracket +] # trailing close bracket ``` ## Output @@ -53,6 +64,16 @@ b3 = [ aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, ] + +# Comment placement in non-empty lists +c1 = [ + # trailing open bracket + # leading item + 1, + # between + 2, # trailing item + # leading close bracket +] # trailing close bracket ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap index a5de9b0da9257..185579f0799a0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap @@ -49,6 +49,14 @@ class TestTrailingComment2: # trailing comment pass +class TestTrailingComment3[T]: # trailing comment + pass + + +class TestTrailingComment4[T](A): # trailing comment + pass + + class Test: """Docstring""" @@ -150,6 +158,60 @@ class AltCLIPOutput( # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP ): ... + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, Cccccccccccccccccccccc]: + pass + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, *Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, **Cccccccccccccccccccccc]: + pass + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: + pass + + +class TestTypeParams[*Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: + pass + + +class TestTypeParams[**Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: + pass + + +class TestTypeParams[**P, *Ts, T]: + pass + + +class TestTypeParams[ # trailing bracket comment + # leading comment + A, + + # in between comment + + B, + # another leading comment + C, + D, # trailing comment + # leading bracket comment +]: + pass + + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](B, C, D): + pass + + + +class TestTypeParams[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, Cccccccccccccccccccccccc, Ddddddddddddddddddddddddd): + pass + + + +class TestTypeParams[A, B, C](meta=Aaaaaaaaaaaaaaaaaaaaaa): + pass ``` ## Output @@ -216,6 +278,14 @@ class TestTrailingComment2: # trailing comment pass +class TestTrailingComment3[T]: # trailing comment + pass + + +class TestTrailingComment4[T](A): # trailing comment + pass + + class Test: """Docstring""" @@ -318,6 +388,77 @@ class AltCLIPOutput( # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP ): ... + + +class TestTypeParams[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + Cccccccccccccccccccccc, +]: + pass + + +class TestTypeParams[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + *Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + **Cccccccccccccccccccccc, +]: + pass + + +class TestTypeParams[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +]: + pass + + +class TestTypeParams[ + *Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +]: + pass + + +class TestTypeParams[ + **Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +]: + pass + + +class TestTypeParams[**P, *Ts, T]: + pass + + +class TestTypeParams[ # trailing bracket comment + # leading comment + A, + # in between comment + B, + # another leading comment + C, + D, # trailing comment + # leading bracket comment +]: + pass + + +class TestTypeParams[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +](B, C, D): + pass + + +class TestTypeParams[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +]( + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + Cccccccccccccccccccccccc, + Ddddddddddddddddddddddddd, +): + pass + + +class TestTypeParams[A, B, C](meta=Aaaaaaaaaaaaaaaaaaaaaa): + pass ``` 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 55dc4d9718c97..9b0021e5ad012 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 @@ -72,6 +72,52 @@ def argument_with_long_type_annotation( def test(): ... +# Type parameter empty line spacing +def test[ + # comment + A, + + # another + + B, +](): ... + + +# Type parameter comments +def type_param_comments[ # trailing bracket comment + # leading comment + A, + + # in between comment + + B, + # another leading comment + C, + D, # trailing comment + # leading bracket comment +](): + # body comment + pass + + +# Note empty type parameters is not valid syntax, e.g. +# def test[](): ... + + +# Different type parameter wrappings + +def single_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccccccccc](): + pass + +def params_on_their_own_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccc, Ddddddddddddd, Eeeeeeee](): + pass + +def param_per_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccccccccc, Ddddddddddddd, Eeeeeeeeeeeeeeeee, ffffffffffff](): + pass + +def single_line_trailing_comma[A, B, C,](): + pass + # Comment def with_leading_comment(): ... @@ -353,6 +399,70 @@ def test(): ... +# Type parameter empty line spacing +def test[ + # comment + A, + # another + B, +](): + ... + + +# Type parameter comments +def type_param_comments[ # trailing bracket comment + # leading comment + A, + # in between comment + B, + # another leading comment + C, + D, # trailing comment + # leading bracket comment +](): + # body comment + pass + + +# Note empty type parameters is not valid syntax, e.g. +# def test[](): ... + + +# Different type parameter wrappings + +def single_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccccccccc](): + pass + + +def params_on_their_own_line[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbb, + Ccccccccccc, + Ddddddddddddd, + Eeeeeeee, +](): + pass + + +def param_per_line[ + Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbb, + Ccccccccccccccccc, + Ddddddddddddd, + Eeeeeeeeeeeeeeeee, + ffffffffffff, +](): + pass + + +def single_line_trailing_comma[ + A, + B, + C, +](): + pass + + # Comment def with_leading_comment(): ...