Add formatter support for call and class definition Arguments (#6274)

## Summary

This PR leverages the `Arguments` AST node introduced in #6259 in the
formatter, which ensures that we correctly handle trailing comments in
calls, like:

```python
f(
  1,
  # comment
)

pass
```

(Previously, this was treated as a leading comment on `pass`.)

This also allows us to unify the argument handling across calls and
class definitions.

## Test Plan

A bunch of new fixture tests, plus improved Black compatibility.
This commit is contained in:
Charlie Marsh 2023-08-02 11:54:22 -04:00 committed by GitHub
parent b095b7204b
commit 4c53bfe896
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 640 additions and 252 deletions

View file

@ -2627,6 +2627,34 @@ impl AstNode for Comprehension {
AnyNode::from(self) AnyNode::from(self)
} }
} }
impl AstNode for Arguments {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::Arguments(node) = kind {
Some(node)
} else {
None
}
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::Arguments(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 Parameters { impl AstNode for Parameters {
fn cast(kind: AnyNode) -> Option<Self> fn cast(kind: AnyNode) -> Option<Self>
where where
@ -3458,6 +3486,11 @@ impl From<Comprehension> for AnyNode {
AnyNode::Comprehension(node) AnyNode::Comprehension(node)
} }
} }
impl From<Arguments> for AnyNode {
fn from(node: Arguments) -> Self {
AnyNode::Arguments(node)
}
}
impl From<Parameters> for AnyNode { impl From<Parameters> for AnyNode {
fn from(node: Parameters) -> Self { fn from(node: Parameters) -> Self {
AnyNode::Parameters(node) AnyNode::Parameters(node)
@ -4909,6 +4942,11 @@ impl<'a> From<&'a Comprehension> for AnyNodeRef<'a> {
AnyNodeRef::Comprehension(node) AnyNodeRef::Comprehension(node)
} }
} }
impl<'a> From<&'a Arguments> for AnyNodeRef<'a> {
fn from(node: &'a Arguments) -> Self {
AnyNodeRef::Arguments(node)
}
}
impl<'a> From<&'a Parameters> for AnyNodeRef<'a> { impl<'a> From<&'a Parameters> for AnyNodeRef<'a> {
fn from(node: &'a Parameters) -> Self { fn from(node: &'a Parameters) -> Self {
AnyNodeRef::Parameters(node) AnyNodeRef::Parameters(node)

View file

@ -86,3 +86,28 @@ f(
f( f(
a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument()
) )
f( # abc
)
f( # abc
# abc
)
f(
# abc
)
f ( # abc
1
)
f (
# abc
1
)
f (
1
# abc
)

View file

@ -92,3 +92,55 @@ class Test:
"""Docstring""" """Docstring"""
# comment # comment
x = 1 x = 1
class C(): # comment
pass
class C( # comment
):
pass
class C(
# comment
):
pass
class C(): # comment
pass
class C( # comment
# comment
1
):
pass
class C(
1
# comment
):
pass
@dataclass
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
class AltCLIPOutput(ModelOutput):
...
@dataclass
class AltCLIPOutput( # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
):
...
@dataclass
class AltCLIPOutput(
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
):
...

View file

@ -3,7 +3,7 @@ use ruff_python_ast::Ranged;
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use crate::comments::{dangling_comments, SourceComment}; use crate::comments::{dangling_comments, trailing_comments, SourceComment};
use crate::context::{NodeLevel, WithNodeLevel}; use crate::context::{NodeLevel, WithNodeLevel};
use crate::prelude::*; use crate::prelude::*;
use crate::MagicTrailingComma; use crate::MagicTrailingComma;
@ -252,7 +252,7 @@ impl<'ast> Format<PyFormatContext<'ast>> for EmptyWithDanglingComments<'_> {
[group(&format_args![ [group(&format_args![
self.opening, self.opening,
// end-of-line comments // end-of-line comments
dangling_comments(&self.comments[..end_of_line_split]), trailing_comments(&self.comments[..end_of_line_split]),
// own line comments, which need to be indented // own line comments, which need to be indented
soft_block_indent(&dangling_comments(&self.comments[end_of_line_split..])), soft_block_indent(&dangling_comments(&self.comments[end_of_line_split..])),
self.closing self.closing

View file

@ -3,8 +3,8 @@ use std::cmp::Ordering;
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::whitespace::indentation; use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Comprehension, Expr, ExprAttribute, ExprBinOp, ExprIfExp, ExprSlice, ExprStarred, self as ast, Arguments, Comprehension, Expr, ExprAttribute, ExprBinOp, ExprIfExp, ExprSlice,
MatchCase, Parameters, Ranged, ExprStarred, MatchCase, Parameters, Ranged,
}; };
use ruff_python_trivia::{ use ruff_python_trivia::{
indentation_at_offset, PythonWhitespace, SimpleToken, SimpleTokenKind, SimpleTokenizer, indentation_at_offset, PythonWhitespace, SimpleToken, SimpleTokenKind, SimpleTokenizer,
@ -46,6 +46,7 @@ pub(super) fn place_comment<'a>(
AnyNodeRef::Parameters(arguments) => { AnyNodeRef::Parameters(arguments) => {
handle_parameters_separator_comment(comment, arguments, locator) handle_parameters_separator_comment(comment, arguments, locator)
} }
AnyNodeRef::Arguments(arguments) => handle_arguments_comment(comment, arguments),
AnyNodeRef::Comprehension(comprehension) => { AnyNodeRef::Comprehension(comprehension) => {
handle_comprehension_comment(comment, comprehension, locator) handle_comprehension_comment(comment, comprehension, locator)
} }
@ -80,6 +81,9 @@ pub(super) fn place_comment<'a>(
AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::StmtAsyncFunctionDef(_) => { AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::StmtAsyncFunctionDef(_) => {
handle_leading_function_with_decorators_comment(comment) handle_leading_function_with_decorators_comment(comment)
} }
AnyNodeRef::StmtClassDef(class_def) => {
handle_leading_class_with_decorators_comment(comment, class_def)
}
AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from), AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from),
_ => CommentPlacement::Default(comment), _ => CommentPlacement::Default(comment),
} }
@ -843,6 +847,32 @@ fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) ->
} }
} }
/// Handle comments between decorators and the decorated node.
///
/// For example, given:
/// ```python
/// @dataclass
/// # comment
/// class Foo(Bar):
/// ...
/// ```
///
/// The comment should be attached to the enclosing [`ast::StmtClassDef`] as a dangling node,
/// as opposed to being treated as a leading comment on `Bar` or similar.
fn handle_leading_class_with_decorators_comment<'a>(
comment: DecoratedComment<'a>,
class_def: &'a ast::StmtClassDef,
) -> CommentPlacement<'a> {
if comment.start() < class_def.name.start() {
if let Some(decorator) = class_def.decorator_list.last() {
if decorator.end() < comment.start() {
return CommentPlacement::dangling(class_def, comment);
}
}
}
CommentPlacement::Default(comment)
}
/// Handles comments between `**` and the variable name in dict unpacking /// Handles comments between `**` and the variable name in dict unpacking
/// It attaches these to the appropriate value node /// It attaches these to the appropriate value node
/// ///
@ -1105,6 +1135,64 @@ fn find_only_token_in_range(
token token
} }
/// Attach an enclosed end-of-line comment to a set of [`Arguments`].
///
/// For example, given:
/// ```python
/// foo( # comment
/// bar,
/// )
/// ```
///
/// The comment will be attached to the [`Arguments`] node as a dangling comment, to ensure
/// that it remains on the same line as open parenthesis.
fn handle_arguments_comment<'a>(
comment: DecoratedComment<'a>,
arguments: &'a Arguments,
) -> CommentPlacement<'a> {
// The comment needs to be on the same line, but before the first argument. For example, we want
// to treat this as a dangling comment:
// ```python
// foo( # comment
// bar,
// baz,
// qux,
// )
// ```
// However, this should _not_ be treated as a dangling comment:
// ```python
// 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 argument. If so, the only possible position is immediately following
// the open parenthesis.
if comment.line_position().is_end_of_line() {
let first_argument = match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
([first_arg, ..], [first_keyword, ..]) => {
if first_arg.start() < first_keyword.start() {
Some(first_arg.range())
} else {
Some(first_keyword.range())
}
}
([first_arg, ..], []) => Some(first_arg.range()),
([], [first_keyword, ..]) => Some(first_keyword.range()),
([], []) => None,
};
if let Some(first_argument) = first_argument {
if arguments.start() < comment.start() && comment.end() < first_argument.start() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}
}
CommentPlacement::Default(comment)
}
/// Attach an enclosed end-of-line comment to a [`StmtImportFrom`]. /// Attach an enclosed end-of-line comment to a [`StmtImportFrom`].
/// ///
/// For example, given: /// For example, given:

View file

@ -1,8 +1,9 @@
use std::iter::Peekable; use std::iter::Peekable;
use ruff_python_ast::{ use ruff_python_ast::{
Alias, Comprehension, Decorator, ElifElseClause, ExceptHandler, Expr, Keyword, MatchCase, Mod, Alias, Arguments, Comprehension, Decorator, ElifElseClause, ExceptHandler, Expr, Keyword,
Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt, TypeParam, WithItem, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt, TypeParam,
WithItem,
}; };
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
@ -229,6 +230,13 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> {
self.finish_node(format_spec); self.finish_node(format_spec);
} }
fn visit_arguments(&mut self, arguments: &'ast Arguments) {
if self.start_node(arguments).is_traverse() {
walk_arguments(self, arguments);
}
self.finish_node(arguments);
}
fn visit_parameters(&mut self, parameters: &'ast Parameters) { fn visit_parameters(&mut self, parameters: &'ast Parameters) {
if self.start_node(parameters).is_traverse() { if self.start_node(parameters).is_traverse() {
walk_parameters(self, parameters); walk_parameters(self, parameters);

View file

@ -1,15 +1,6 @@
use ruff_python_ast::{Arguments, Expr, ExprCall, Ranged};
use ruff_text_size::{TextRange, TextSize};
use crate::builders::empty_parenthesized_with_dangling_comments;
use ruff_formatter::write; use ruff_formatter::write;
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::ExprCall;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
use crate::expression::parentheses::{
parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
use crate::prelude::*; use crate::prelude::*;
use crate::FormatNodeRule; use crate::FormatNodeRule;
@ -21,136 +12,9 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
let ExprCall { let ExprCall {
range: _, range: _,
func, func,
arguments: arguments,
Arguments {
args,
keywords,
range: _,
},
} = item; } = item;
// We have a case with `f()` without any argument, which is a special case because we can write!(f, [func.format(), arguments.format()])
// have a comment with no node attachment inside:
// ```python
// f(
// # This function has a dangling comment
// )
// ```
if args.is_empty() && keywords.is_empty() {
let comments = f.context().comments().clone();
return write!(
f,
[
func.format(),
empty_parenthesized_with_dangling_comments(
text("("),
comments.dangling_comments(item),
text(")"),
)
]
);
}
let all_args = format_with(|f: &mut PyFormatter| {
let source = f.context().source();
let mut joiner = f.join_comma_separated(item.end());
match args.as_slice() {
[argument] if keywords.is_empty() => {
match argument {
Expr::GeneratorExp(generator_exp) => joiner.entry(
generator_exp,
&generator_exp
.format()
.with_options(GeneratorExpParentheses::StripIfOnlyFunctionArg),
),
other => {
let parentheses =
if is_single_argument_parenthesized(argument, item.end(), source) {
Parentheses::Always
} else {
Parentheses::Never
};
joiner.entry(other, &other.format().with_options(parentheses))
}
};
}
arguments => {
joiner
.entries(
// We have the parentheses from the call so the arguments never need any
arguments
.iter()
.map(|arg| (arg, arg.format().with_options(Parentheses::Preserve))),
)
.nodes(keywords.iter());
}
}
joiner.finish()
});
write!(
f,
[
func.format(),
// The outer group is for things like
// ```python
// get_collection(
// hey_this_is_a_very_long_call,
// it_has_funny_attributes_asdf_asdf,
// too_long_for_the_line,
// really=True,
// )
// ```
// The inner group is for things like:
// ```python
// get_collection(
// hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True
// )
// ```
// TODO(konstin): Doesn't work see wrongly formatted test
parenthesized("(", &group(&all_args), ")")
]
)
}
fn fmt_dangling_comments(&self, _node: &ExprCall, _f: &mut PyFormatter) -> FormatResult<()> {
// Handled in `fmt_fields`
Ok(())
} }
} }
impl NeedsParentheses for ExprCall {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
self.func.needs_parentheses(self.into(), context)
}
}
fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: &str) -> bool {
let mut has_seen_r_paren = false;
for token in
SimpleTokenizer::new(source, TextRange::new(argument.end(), call_end)).skip_trivia()
{
match token.kind() {
SimpleTokenKind::RParen => {
if has_seen_r_paren {
return true;
}
has_seen_r_paren = true;
}
// Skip over any trailing comma
SimpleTokenKind::Comma => continue,
_ => {
// Passed the arguments
break;
}
}
}
false
}

View file

@ -2617,6 +2617,38 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::Comprehension {
} }
} }
impl FormatRule<ast::Arguments, PyFormatContext<'_>> for crate::other::arguments::FormatArguments {
#[inline]
fn fmt(
&self,
node: &ast::Arguments,
f: &mut Formatter<PyFormatContext<'_>>,
) -> FormatResult<()> {
FormatNodeRule::<ast::Arguments>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::Arguments {
type Format<'a> = FormatRefWithRule<
'a,
ast::Arguments,
crate::other::arguments::FormatArguments,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, crate::other::arguments::FormatArguments::default())
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::Arguments {
type Format = FormatOwnedWithRule<
ast::Arguments,
crate::other::arguments::FormatArguments,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, crate::other::arguments::FormatArguments::default())
}
}
impl FormatRule<ast::Parameters, PyFormatContext<'_>> impl FormatRule<ast::Parameters, PyFormatContext<'_>>
for crate::other::parameters::FormatParameters for crate::other::parameters::FormatParameters
{ {

View file

@ -0,0 +1,152 @@
use ruff_formatter::write;
use ruff_python_ast::node::{AnyNodeRef, AstNode};
use ruff_python_ast::{Arguments, Expr, ExprCall, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
use crate::builders::empty_parenthesized_with_dangling_comments;
use crate::comments::trailing_comments;
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
use crate::expression::parentheses::{
parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
use crate::prelude::*;
use crate::FormatNodeRule;
#[derive(Default)]
pub struct FormatArguments;
impl FormatNodeRule<Arguments> for FormatArguments {
fn fmt_fields(&self, item: &Arguments, f: &mut PyFormatter) -> FormatResult<()> {
// We have a case with `f()` without any argument, which is a special case because we can
// have a comment with no node attachment inside:
// ```python
// f(
// # This call has a dangling comment.
// )
// ```
if item.args.is_empty() && item.keywords.is_empty() {
let comments = f.context().comments().clone();
return write!(
f,
[empty_parenthesized_with_dangling_comments(
text("("),
comments.dangling_comments(item),
text(")"),
)]
);
}
// If the arguments are non-empty, then a dangling comment indicates a comment on the
// same line as the opening parenthesis, e.g.:
// ```python
// f( # This call 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 all_arguments = format_with(|f: &mut PyFormatter| {
let source = f.context().source();
let mut joiner = f.join_comma_separated(item.end());
match item.args.as_slice() {
[arg] if item.keywords.is_empty() => {
match arg {
Expr::GeneratorExp(generator_exp) => joiner.entry(
generator_exp,
&generator_exp
.format()
.with_options(GeneratorExpParentheses::StripIfOnlyFunctionArg),
),
other => {
let parentheses =
if is_single_argument_parenthesized(arg, item.end(), source) {
Parentheses::Always
} else {
Parentheses::Never
};
joiner.entry(other, &other.format().with_options(parentheses))
}
};
}
args => {
joiner
.entries(
// We have the parentheses from the call so the item never need any
args.iter()
.map(|arg| (arg, arg.format().with_options(Parentheses::Preserve))),
)
.nodes(item.keywords.iter());
}
}
joiner.finish()
});
write!(
f,
[
// The outer group is for things like
// ```python
// get_collection(
// hey_this_is_a_very_long_call,
// it_has_funny_attributes_asdf_asdf,
// too_long_for_the_line,
// really=True,
// )
// ```
// The inner group is for things like:
// ```python
// get_collection(
// hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True
// )
// ```
// TODO(konstin): Doesn't work see wrongly formatted test
parenthesized("(", &group(&all_arguments), ")")
]
)
}
fn fmt_dangling_comments(&self, _node: &Arguments, _f: &mut PyFormatter) -> FormatResult<()> {
// Handled in `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for ExprCall {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
self.func.needs_parentheses(self.into(), context)
}
}
fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: &str) -> bool {
let mut has_seen_r_paren = false;
for token in
SimpleTokenizer::new(source, TextRange::new(argument.end(), call_end)).skip_trivia()
{
match token.kind() {
SimpleTokenKind::RParen => {
if has_seen_r_paren {
return true;
}
has_seen_r_paren = true;
}
// Skip over any trailing comma
SimpleTokenKind::Comma => continue,
_ => {
// Passed the arguments
break;
}
}
}
false
}

View file

@ -1,4 +1,5 @@
pub(crate) mod alias; pub(crate) mod alias;
pub(crate) mod arguments;
pub(crate) mod comprehension; pub(crate) mod comprehension;
pub(crate) mod decorator; pub(crate) mod decorator;
pub(crate) mod elif_else_clause; pub(crate) mod elif_else_clause;

View file

@ -1,11 +1,8 @@
use ruff_python_ast::{Arguments, Ranged, StmtClassDef};
use ruff_text_size::TextRange;
use ruff_formatter::write; use ruff_formatter::write;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_ast::{Ranged, StmtClassDef};
use ruff_python_trivia::{lines_after, skip_trailing_trivia};
use crate::comments::trailing_comments; use crate::comments::{leading_comments, trailing_comments};
use crate::expression::parentheses::{parenthesized, Parentheses};
use crate::prelude::*; use crate::prelude::*;
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
@ -23,40 +20,84 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
decorator_list, decorator_list,
} = item; } = item;
f.join_with(hard_line_break()) let comments = f.context().comments().clone();
.entries(decorator_list.iter().formatted())
.finish()?;
if !decorator_list.is_empty() { let dangling_comments = comments.dangling_comments(item);
hard_line_break().fmt(f)?; let trailing_definition_comments_start =
dangling_comments.partition_point(|comment| comment.line_position().is_own_line());
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);
if let Some(last_decorator) = decorator_list.last() {
f.join_with(hard_line_break())
.entries(decorator_list.iter().formatted())
.finish()?;
if leading_definition_comments.is_empty() {
write!(f, [hard_line_break()])?;
} else {
// Write any leading definition comments (between last decorator and the header)
// while maintaining the right amount of empty lines between the comment
// and the last decorator.
let decorator_end =
skip_trailing_trivia(last_decorator.end(), f.context().source());
let leading_line = if lines_after(decorator_end, f.context().source()) <= 1 {
hard_line_break()
} else {
empty_line()
};
write!(
f,
[leading_line, leading_comments(leading_definition_comments)]
)?;
}
} }
write!(f, [text("class"), space(), name.format()])?; write!(f, [text("class"), space(), name.format()])?;
if arguments if let Some(arguments) = arguments {
.as_ref() // Drop empty parentheses, e.g., in:
.is_some_and(|Arguments { args, keywords, .. }| { // ```python
!(args.is_empty() && keywords.is_empty()) // class A():
}) // ...
{ // ```
parenthesized( //
"(", // However, preserve any dangling end-of-line comments, e.g., in:
&FormatInheritanceClause { // ```python
class_definition: item, // class A( # comment
}, // ):
")", // ...
) //
.fmt(f)?; // If the arguments contain any dangling own-line comments, we retain the parentheses,
// e.g., in:
// ```python
// class A( # comment
// # comment
// ):
// ...
// ```
if arguments.args.is_empty()
&& arguments.keywords.is_empty()
&& comments
.dangling_comments(arguments)
.iter()
.all(|comment| comment.line_position().is_end_of_line())
{
let dangling = comments.dangling_comments(arguments);
write!(f, [trailing_comments(dangling)])?;
} else {
write!(f, [arguments.format()])?;
}
} }
let comments = f.context().comments().clone();
let trailing_head_comments = comments.dangling_comments(item);
write!( write!(
f, f,
[ [
text(":"), text(":"),
trailing_comments(trailing_head_comments), trailing_comments(trailing_definition_comments),
block_indent(&body.format().with_options(SuiteKind::Class)) block_indent(&body.format().with_options(SuiteKind::Class))
] ]
) )
@ -71,58 +112,3 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
Ok(()) Ok(())
} }
} }
struct FormatInheritanceClause<'a> {
class_definition: &'a StmtClassDef,
}
impl Format<PyFormatContext<'_>> for FormatInheritanceClause<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let StmtClassDef {
arguments:
Some(Arguments {
args: bases,
keywords,
..
}),
name,
body,
..
} = self.class_definition
else {
return Ok(());
};
let source = f.context().source();
let mut joiner = f.join_comma_separated(body.first().unwrap().start());
if let Some((first, rest)) = bases.split_first() {
// Manually handle parentheses for the first expression because the logic in `FormatExpr`
// doesn't know that it should disregard the parentheses of the inheritance clause.
// ```python
// class Test(A) # A is not parenthesized, the parentheses belong to the inheritance clause
// class Test((A)) # A is parenthesized
// ```
// parentheses from the inheritance clause belong to the expression.
let tokenizer = SimpleTokenizer::new(source, TextRange::new(name.end(), first.start()))
.skip_trivia();
let left_paren_count = tokenizer
.take_while(|token| token.kind() == SimpleTokenKind::LParen)
.count();
// Ignore the first parentheses count
let parentheses = if left_paren_count > 1 {
Parentheses::Always
} else {
Parentheses::Never
};
joiner.entry(first, &first.format().with_options(parentheses));
joiner.nodes(rest.iter());
}
joiner.nodes(keywords.iter()).finish()
}
}

View file

@ -44,7 +44,7 @@ impl FormatRule<AnyFunctionDefinition<'_>, PyFormatContext<'_>> for FormatAnyFun
let trailing_definition_comments_start = let trailing_definition_comments_start =
dangling_comments.partition_point(|comment| comment.line_position().is_own_line()); dangling_comments.partition_point(|comment| comment.line_position().is_own_line());
let (leading_function_definition_comments, trailing_definition_comments) = let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start); dangling_comments.split_at(trailing_definition_comments_start);
if let Some(last_decorator) = item.decorators().last() { if let Some(last_decorator) = item.decorators().last() {
@ -52,10 +52,10 @@ impl FormatRule<AnyFunctionDefinition<'_>, PyFormatContext<'_>> for FormatAnyFun
.entries(item.decorators().iter().formatted()) .entries(item.decorators().iter().formatted())
.finish()?; .finish()?;
if leading_function_definition_comments.is_empty() { if leading_definition_comments.is_empty() {
write!(f, [hard_line_break()])?; write!(f, [hard_line_break()])?;
} else { } else {
// Write any leading function comments (between last decorator and function header) // Write any leading definition comments (between last decorator and the header)
// while maintaining the right amount of empty lines between the comment // while maintaining the right amount of empty lines between the comment
// and the last decorator. // and the last decorator.
let decorator_end = let decorator_end =
@ -69,10 +69,7 @@ impl FormatRule<AnyFunctionDefinition<'_>, PyFormatContext<'_>> for FormatAnyFun
write!( write!(
f, f,
[ [leading_line, leading_comments(leading_definition_comments)]
leading_line,
leading_comments(leading_function_definition_comments)
]
)?; )?;
} }
} }

View file

@ -92,6 +92,31 @@ f(
f( f(
a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument()
) )
f( # abc
)
f( # abc
# abc
)
f(
# abc
)
f ( # abc
1
)
f (
# abc
1
)
f (
1
# abc
)
``` ```
## Output ## Output
@ -137,8 +162,9 @@ f(
these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session,
) )
f() f(
# dangling comment # dangling comment
)
f(only=1, short=1, arguments=1) f(only=1, short=1, arguments=1)
@ -177,6 +203,28 @@ f(
f( f(
a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument()
) )
f() # abc
f( # abc
# abc
)
f(
# abc
)
f(1) # abc
f(
# abc
1
)
f(
1
# abc
)
``` ```

View file

@ -130,8 +130,7 @@ a = {
3: True, 3: True,
} }
x = { # dangling end of line comment x = {} # dangling end of line comment
}
``` ```

View file

@ -149,8 +149,7 @@ a = (
# Regression test: lambda empty arguments ranges were too long, leading to unstable # Regression test: lambda empty arguments ranges were too long, leading to unstable
# formatting # formatting
( (
lambda: ( # lambda: (), #
),
) )

View file

@ -33,8 +33,7 @@ b3 = [
```py ```py
# Dangling comment placement in empty lists # Dangling comment placement in empty lists
# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 # Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16
a1 = [ # a a1 = [] # a
]
a2 = [ # a a2 = [ # a
# b # b
] ]

View file

@ -98,6 +98,58 @@ class Test:
"""Docstring""" """Docstring"""
# comment # comment
x = 1 x = 1
class C(): # comment
pass
class C( # comment
):
pass
class C(
# comment
):
pass
class C(): # comment
pass
class C( # comment
# comment
1
):
pass
class C(
1
# comment
):
pass
@dataclass
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
class AltCLIPOutput(ModelOutput):
...
@dataclass
class AltCLIPOutput( # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
):
...
@dataclass
class AltCLIPOutput(
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
):
...
``` ```
## Output ## Output
@ -116,8 +168,7 @@ class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta):
pass pass
class Test( class Test( # trailing class comment
# trailing class comment
Aaaaaaaaaaaaaaaaa, # trailing comment Aaaaaaaaaaaaaaaaa, # trailing comment
# in between comment # in between comment
Bbbbbbbbbbbbbbbb, Bbbbbbbbbbbbbbbb,
@ -217,6 +268,56 @@ class Test:
# comment # comment
x = 1 x = 1
class C: # comment
pass
class C: # comment
pass
class C(
# comment
):
pass
class C: # comment
pass
class C( # comment
# comment
1
):
pass
class C(
1
# comment
):
pass
@dataclass
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
class AltCLIPOutput(ModelOutput):
...
@dataclass
class AltCLIPOutput: # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
...
@dataclass
class AltCLIPOutput(
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
):
...
``` ```

View file

@ -215,8 +215,7 @@ del (
) # Completed ) # Completed
# Done # Done
del ( # dangling end of line comment del () # dangling end of line comment
)
``` ```

View file

@ -192,8 +192,7 @@ raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfk < (
) # the other end ) # the other end
# sneaky comment # sneaky comment
raise ( # another comment raise () # another comment
)
raise () # what now raise () # what now
@ -201,8 +200,9 @@ raise ( # sould I stay here
# just a comment here # just a comment here
) # trailing comment ) # trailing comment
raise hello() # sould I stay here raise hello( # sould I stay here
# just a comment here # trailing comment # just a comment here
) # trailing comment
raise ( raise (
# sould I stay here # sould I stay here