Compare commits

...

4 Commits

Author SHA1 Message Date
konstin
d4cfe1f2a7 Break before slice colon
**Summary** Break slices at the colon first, since the colon is separator with the lowest precedence and we're in a parenthesized context.

**Input**
```python
section_header_data = byte_array[byte_begin_index + byte_step_index * event_index : byte_begin_index + byte_step_index * (event_index + 1)]
```
**Black**
```python
section_header_data = byte_array[
    byte_begin_index
    + byte_step_index * event_index : byte_begin_index
    + byte_step_index * (event_index + 1)
]
```
**Current formatting**
```python
section_header_data = byte_array[
    byte_begin_index + byte_step_index * event_index : byte_begin_index
    + byte_step_index * (event_index + 1)
]
```
**Proposed formatting**
```python
section_header_data = byte_array[
    byte_begin_index + byte_step_index * event_index
    : byte_begin_index + byte_step_index * (event_index + 1)
]
```

This is another intentional black deviation, but i find it a clear style improvement.

This is consistent with adding a step:
```python
section_header_data2 = byte_array[
    byte_begin_index + byte_step_index * event_index
    : byte_begin_index + byte_step_index
    : section_size
]
```

As-is, this regresses trailing colon comments:

**in**
```python
c1 = "c"[
    1:  # e
    # f
    2
]
```
**out**
```python
c1 = "c"[
    1
    :  # e
    # f
    2
]
```

Fixes #7316

**Test Plan** Added the fixtures above.
2023-09-13 12:11:53 +02:00
konsti
f4c7bff36b Don't reorder parameters in function calls (#7268)
## Summary

In `f(*args, a=b, *args2, **kwargs)` the args (`*args`, `*args2`) and
keywords (`a=b`, `**kwargs`) are interleaved, which we previously didn't
handle.

Fixes #6498

**main**

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| **django** | 0.99966 | 2760 | 58 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |

**PR**

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| **django** | 0.99967 | 2760 | 53 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |


## Test Plan

New fixtures
2023-09-13 09:01:49 +00:00
konsti
56440ad835 Introduce ArgOrKeyword to keep call parameter order (#7302)
## Motivation

The `ast::Arguments` for call argument are split into positional
arguments (args) and keywords arguments (keywords). We currently assume
that call consists of first args and then keywords, which is generally
the case, but not always:

```python
f(*args, a=2, *args2, **kwargs)

class A(*args, a=2, *args2, **kwargs):
    pass
```

The consequence is accidentally reordering arguments
(https://github.com/astral-sh/ruff/pull/7268).

## Summary

`Arguments::args_and_keywords` returns an iterator of an `ArgOrKeyword`
enum that yields args and keywords in the correct order. I've fixed the
obvious `args` and `keywords` usages, but there might be some cases with
wrong assumptions remaining.

## Test Plan

The generator got new test cases, otherwise the stacked PR
(https://github.com/astral-sh/ruff/pull/7268) which uncovered this.
2023-09-13 08:45:46 +00:00
Charlie Marsh
179128dc54 Link discussion in formatter README (#7311) 2023-09-12 16:50:22 +00:00
18 changed files with 400 additions and 191 deletions

1
Cargo.lock generated
View File

@@ -2304,6 +2304,7 @@ dependencies = [
"bitflags 2.4.0",
"insta",
"is-macro",
"itertools",
"memchr",
"num-bigint",
"num-traits",

View File

@@ -3,7 +3,7 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
@@ -92,10 +92,8 @@ pub(crate) fn remove_argument<T: Ranged>(
) -> Result<Edit> {
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
.args
.iter()
.map(Expr::range)
.chain(arguments.keywords.iter().map(Keyword::range))
.arguments_source_order()
.map(|arg| arg.range())
.filter(|range| argument.range() != *range)
.partition(|range| range.start() < argument.start());

View File

@@ -19,6 +19,7 @@ ruff_text_size = { path = "../ruff_text_size" }
bitflags = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }

View File

@@ -207,6 +207,8 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
range: _,
}) => {
any_over_expr(call_func, func)
// Note that this is the evaluation order but not necessarily the declaration order
// (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not)
|| args.iter().any(|expr| any_over_expr(expr, func))
|| keywords
.iter()
@@ -347,6 +349,8 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool {
decorator_list,
..
}) => {
// Note that e.g. `class A(*args, a=2, *args2, **kwargs): pass` is a valid class
// definition
arguments
.as_deref()
.is_some_and(|Arguments { args, keywords, .. }| {

View File

@@ -1,9 +1,9 @@
use crate::visitor::preorder::PreorderVisitor;
use crate::{
self as ast, Alias, Arguments, Comprehension, Decorator, ExceptHandler, Expr, Keyword,
MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments,
PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
TypeParams, WithItem,
self as ast, Alias, ArgOrKeyword, Arguments, Comprehension, Decorator, ExceptHandler, Expr,
Keyword, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern,
PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, TypeParams, WithItem,
};
use ruff_text_size::{Ranged, TextRange};
use std::ptr::NonNull;
@@ -3549,18 +3549,11 @@ impl AstNode for Arguments {
where
V: PreorderVisitor<'a> + ?Sized,
{
let ast::Arguments {
range: _,
args,
keywords,
} = self;
for arg in args {
visitor.visit_expr(arg);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
for arg_or_keyword in self.arguments_source_order() {
match arg_or_keyword {
ArgOrKeyword::Arg(arg) => visitor.visit_expr(arg),
ArgOrKeyword::Keyword(keyword) => visitor.visit_keyword(keyword),
}
}
}
}

View File

@@ -1,5 +1,6 @@
#![allow(clippy::derive_partial_eq_without_eq)]
use itertools::Itertools;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
@@ -2177,6 +2178,34 @@ pub struct Arguments {
pub keywords: Vec<Keyword>,
}
/// An entry in the argument list of a function call.
#[derive(Clone, Debug, PartialEq)]
pub enum ArgOrKeyword<'a> {
Arg(&'a Expr),
Keyword(&'a Keyword),
}
impl<'a> From<&'a Expr> for ArgOrKeyword<'a> {
fn from(arg: &'a Expr) -> Self {
Self::Arg(arg)
}
}
impl<'a> From<&'a Keyword> for ArgOrKeyword<'a> {
fn from(keyword: &'a Keyword) -> Self {
Self::Keyword(keyword)
}
}
impl Ranged for ArgOrKeyword<'_> {
fn range(&self) -> TextRange {
match self {
Self::Arg(arg) => arg.range(),
Self::Keyword(keyword) => keyword.range(),
}
}
}
impl Arguments {
/// Return the number of positional and keyword arguments.
pub fn len(&self) -> usize {
@@ -2212,6 +2241,46 @@ impl Arguments {
.map(|keyword| &keyword.value)
.or_else(|| self.find_positional(position))
}
/// Return the positional and keyword arguments in the order of declaration.
///
/// Positional arguments are generally before keyword arguments, but star arguments are an
/// exception:
/// ```python
/// class A(*args, a=2, *args2, **kwargs):
/// pass
///
/// f(*args, a=2, *args2, **kwargs)
/// ```
/// where `*args` and `args2` are `args` while `a=1` and `kwargs` are `keywords`.
///
/// If you would just chain `args` and `keywords` the call would get reordered which we don't
/// want. This function instead "merge sorts" them into the correct order.
///
/// Note that the order of evaluation is always first `args`, then `keywords`:
/// ```python
/// def f(*args, **kwargs):
/// pass
///
/// def g(x):
/// print(x)
/// return x
///
///
/// f(*g([1]), a=g(2), *g([3]), **g({"4": 5}))
/// ```
/// Output:
/// ```text
/// [1]
/// [3]
/// 2
/// {'4': 5}
/// ```
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
let args = self.args.iter().map(ArgOrKeyword::Arg);
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
args.merge_by(keywords, |left, right| left.start() < right.start())
}
}
/// An AST node used to represent a sequence of type parameters.

View File

@@ -573,6 +573,9 @@ pub fn walk_format_spec<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, format_spe
}
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
// Note that the there might be keywords before the last arg, e.g. in
// f(*args, a=2, *args2, **kwargs)`, but we follow Python in evaluating first `args` and then
// `keywords`. See also [Arguments::arguments_source_order`].
for arg in &arguments.args {
visitor.visit_expr(arg);
}

View File

@@ -3,9 +3,10 @@
use std::ops::Deref;
use ruff_python_ast::{
self as ast, Alias, BoolOp, CmpOp, Comprehension, Constant, ConversionFlag, DebugText,
ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters, Pattern, Stmt,
Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem,
self as ast, Alias, ArgOrKeyword, BoolOp, CmpOp, Comprehension, Constant, ConversionFlag,
DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters,
Pattern, Stmt, Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
WithItem,
};
use ruff_python_ast::{ParameterWithDefault, TypeParams};
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
@@ -265,19 +266,23 @@ impl<'a> Generator<'a> {
if let Some(arguments) = arguments {
self.p("(");
let mut first = true;
for base in &arguments.args {
self.p_delim(&mut first, ", ");
self.unparse_expr(base, precedence::MAX);
}
for keyword in &arguments.keywords {
self.p_delim(&mut first, ", ");
if let Some(arg) = &keyword.arg {
self.p_id(arg);
self.p("=");
} else {
self.p("**");
for arg_or_keyword in arguments.arguments_source_order() {
match arg_or_keyword {
ArgOrKeyword::Arg(arg) => {
self.p_delim(&mut first, ", ");
self.unparse_expr(arg, precedence::MAX);
}
ArgOrKeyword::Keyword(keyword) => {
self.p_delim(&mut first, ", ");
if let Some(arg) = &keyword.arg {
self.p_id(arg);
self.p("=");
} else {
self.p("**");
}
self.unparse_expr(&keyword.value, precedence::MAX);
}
}
self.unparse_expr(&keyword.value, precedence::MAX);
}
self.p(")");
}
@@ -1045,19 +1050,24 @@ impl<'a> Generator<'a> {
self.unparse_comp(generators);
} else {
let mut first = true;
for arg in &arguments.args {
self.p_delim(&mut first, ", ");
self.unparse_expr(arg, precedence::COMMA);
}
for kw in &arguments.keywords {
self.p_delim(&mut first, ", ");
if let Some(arg) = &kw.arg {
self.p_id(arg);
self.p("=");
self.unparse_expr(&kw.value, precedence::COMMA);
} else {
self.p("**");
self.unparse_expr(&kw.value, precedence::MAX);
for arg_or_keyword in arguments.arguments_source_order() {
match arg_or_keyword {
ArgOrKeyword::Arg(arg) => {
self.p_delim(&mut first, ", ");
self.unparse_expr(arg, precedence::COMMA);
}
ArgOrKeyword::Keyword(keyword) => {
self.p_delim(&mut first, ", ");
if let Some(arg) = &keyword.arg {
self.p_id(arg);
self.p("=");
self.unparse_expr(&keyword.value, precedence::COMMA);
} else {
self.p("**");
self.unparse_expr(&keyword.value, precedence::MAX);
}
}
}
}
}
@@ -1649,6 +1659,11 @@ class Foo:
assert_round_trip!(r#"type Foo[*Ts] = ..."#);
assert_round_trip!(r#"type Foo[**P] = ..."#);
assert_round_trip!(r#"type Foo[T, U, *Ts, **P] = ..."#);
// https://github.com/astral-sh/ruff/issues/6498
assert_round_trip!(r#"f(a=1, *args, **kwargs)"#);
assert_round_trip!(r#"f(*args, a=1, **kwargs)"#);
assert_round_trip!(r#"f(*args, a=1, *args2, **kwargs)"#);
assert_round_trip!("class A(*args, a=2, *args2, **kwargs):\n pass");
}
#[test]

View File

@@ -1,10 +1,14 @@
# Ruff Formatter
The Ruff formatter is an extremely fast Python code formatter that ships as part of the `ruff`
CLI (as of Ruff v0.0.287).
CLI (as of Ruff v0.0.289).
The formatter is currently in an **alpha** state. As such, it's not yet recommended for production
use, but it _is_ ready for experimentation and testing. _We'd love to have your feedback._
The formatter is currently in an **Alpha** state. The Alpha is primarily intended for
experimentation: our focus is on collecting feedback that we can address prior to a production-ready
Beta release later this year. (While we're using the formatter in production on our own projects,
the CLI, configuration options, and code style may change arbitrarily between the Alpha and Beta.)
[_We'd love to hear your feedback._](https://github.com/astral-sh/ruff/discussions/7310)
## Goals
@@ -26,7 +30,7 @@ For details, see [Black compatibility](#black-compatibility).
## Getting started
The Ruff formatter shipped in an alpha state as part of Ruff v0.0.287.
The Ruff formatter shipped in an Alpha state as part of Ruff v0.0.289.
### CLI
@@ -69,8 +73,7 @@ instead exiting with a non-zero status code if any files are not already formatt
### VS Code
As of `v2023.34.0`,
the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff)
As of `v2023.36.0`, the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff)
ships with support for the Ruff formatter. To enable formatting capabilities, set the
`ruff.enableExperimentalFormatter` setting to `true` in your `settings.json`, and mark the Ruff
extension as your default Python formatter:

View File

@@ -242,3 +242,26 @@ f(x=(
# comment
1
))
args = [2]
args2 = [3]
kwargs = {"4": 5}
# https://github.com/astral-sh/ruff/issues/6498
f(a=1, *args, **kwargs)
f(*args, a=1, **kwargs)
f(*args, a=1, *args2, **kwargs)
f( # a
* # b
args
# c
, # d
a=1,
# e
* # f
args2
# g
** # h
kwargs,
)

View File

@@ -91,3 +91,18 @@ f = "f"[:,]
g1 = "g"[(1):(2)]
g2 = "g"[(1):(2):(3)]
# https://github.com/astral-sh/ruff/issues/7316
section_header_data = byte_array[
byte_begin_index
+ byte_step_index * event_index : byte_begin_index
+ byte_step_index * (event_index + 1)
]
section_header_data2 = byte_array[
byte_begin_index
+ byte_step_index * event_index : byte_begin_index
+ byte_step_index : section_size
]

View File

@@ -38,7 +38,7 @@ pub(super) fn place_comment<'a>(
/// ):
/// ...
/// ```
/// The parentheses enclose `True`, but the range of `True`doesn't include the `# comment`.
/// The parentheses enclose `True`, but the range of `True` doesn't include the `# comment`.
///
/// Default handling can get parenthesized comments wrong in a number of ways. For example, the
/// comment here is marked (by default) as a trailing comment of `x`, when it should be a leading
@@ -120,10 +120,8 @@ fn handle_parenthesized_comment<'a>(
// For now, we _can_ assert, but to do so, we stop lexing when we hit a token that precedes an
// identifier.
if comment.line_position().is_end_of_line() {
let tokenizer = SimpleTokenizer::new(
locator.contents(),
TextRange::new(preceding.end(), comment.start()),
);
let range = TextRange::new(preceding.end(), comment.start());
let tokenizer = SimpleTokenizer::new(locator.contents(), range);
if tokenizer
.skip_trivia()
.take_while(|token| {
@@ -136,7 +134,7 @@ fn handle_parenthesized_comment<'a>(
debug_assert!(
!matches!(token.kind, SimpleTokenKind::Bogus),
"Unexpected token between nodes: `{:?}`",
locator.slice(TextRange::new(preceding.end(), comment.start()),)
locator.slice(range)
);
token.kind() == SimpleTokenKind::LParen
@@ -145,10 +143,8 @@ fn handle_parenthesized_comment<'a>(
return CommentPlacement::leading(following, comment);
}
} else {
let tokenizer = SimpleTokenizer::new(
locator.contents(),
TextRange::new(comment.end(), following.start()),
);
let range = TextRange::new(comment.end(), following.start());
let tokenizer = SimpleTokenizer::new(locator.contents(), range);
if tokenizer
.skip_trivia()
.take_while(|token| {
@@ -161,7 +157,7 @@ fn handle_parenthesized_comment<'a>(
debug_assert!(
!matches!(token.kind, SimpleTokenKind::Bogus),
"Unexpected token between nodes: `{:?}`",
locator.slice(TextRange::new(comment.end(), following.start()))
locator.slice(range)
);
token.kind() == SimpleTokenKind::RParen
})

View File

@@ -91,7 +91,7 @@ impl FormatNodeRule<ExprSlice> for FormatExprSlice {
if !all_simple && lower.is_some() {
space().fmt(f)?;
}
token(":").fmt(f)?;
write!(f, [soft_line_break(), token(":")])?;
// No upper node, no need for a space, e.g. `x[a() :]`
if !all_simple && upper.is_some() {
space().fmt(f)?;
@@ -125,7 +125,7 @@ impl FormatNodeRule<ExprSlice> for FormatExprSlice {
if !all_simple && (upper.is_some() || step.is_none()) {
space().fmt(f)?;
}
token(":").fmt(f)?;
write!(f, [soft_line_break(), token(":")])?;
// No step node, no need for a space
if !all_simple && step.is_some() {
space().fmt(f)?;

View File

@@ -1,6 +1,5 @@
use ruff_formatter::write;
use ruff_python_ast::node::AstNode;
use ruff_python_ast::{Arguments, Expr};
use ruff_python_ast::{ArgOrKeyword, Arguments, Expr};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -14,6 +13,11 @@ pub struct FormatArguments;
impl FormatNodeRule<Arguments> for FormatArguments {
fn fmt_fields(&self, item: &Arguments, f: &mut PyFormatter) -> FormatResult<()> {
let Arguments {
range,
args,
keywords,
} = item;
// 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
@@ -21,7 +25,7 @@ impl FormatNodeRule<Arguments> for FormatArguments {
// # This call has a dangling comment.
// )
// ```
if item.args.is_empty() && item.keywords.is_empty() {
if args.is_empty() && keywords.is_empty() {
let comments = f.context().comments().clone();
let dangling = comments.dangling(item);
return write!(f, [empty_parenthesized("(", dangling, ")")]);
@@ -29,9 +33,9 @@ impl FormatNodeRule<Arguments> for FormatArguments {
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() => {
let mut joiner = f.join_comma_separated(range.end());
match args.as_slice() {
[arg] if keywords.is_empty() => {
match arg {
Expr::GeneratorExp(generator_exp) => joiner.entry(
generator_exp,
@@ -41,7 +45,7 @@ impl FormatNodeRule<Arguments> for FormatArguments {
),
other => {
let parentheses =
if is_single_argument_parenthesized(arg, item.end(), source) {
if is_single_argument_parenthesized(arg, range.end(), source) {
Parentheses::Always
} else {
// Note: no need to handle opening-parenthesis comments, since
@@ -53,14 +57,17 @@ impl FormatNodeRule<Arguments> for FormatArguments {
}
};
}
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());
_ => {
for arg_or_keyword in item.arguments_source_order() {
match arg_or_keyword {
ArgOrKeyword::Arg(arg) => {
joiner.entry(arg, &arg.format());
}
ArgOrKeyword::Keyword(keyword) => {
joiner.entry(keyword, &keyword.format());
}
}
}
}
}
@@ -76,7 +83,7 @@ impl FormatNodeRule<Arguments> for FormatArguments {
// c,
// )
let comments = f.context().comments().clone();
let dangling_comments = comments.dangling(item.as_any_node_ref());
let dangling_comments = comments.dangling(item);
write!(
f,

View File

@@ -18,14 +18,9 @@ pub struct FormatStmtFunctionDef;
impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
fn fmt_fields(&self, item: &StmtFunctionDef, f: &mut PyFormatter) -> FormatResult<()> {
let StmtFunctionDef {
range: _,
is_async,
decorator_list,
name,
type_params,
parameters,
returns,
body,
..
} = item;
let comments = f.context().comments().clone();
@@ -47,101 +42,7 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
clause_header(
ClauseHeader::Function(item),
trailing_definition_comments,
&format_with(|f| {
if *is_async {
write!(f, [token("async"), space()])?;
}
write!(f, [token("def"), space(), name.format()])?;
if let Some(type_params) = type_params.as_ref() {
write!(f, [type_params.format()])?;
}
let format_inner = format_with(|f: &mut PyFormatter| {
write!(f, [parameters.format()])?;
if let Some(return_annotation) = returns.as_ref() {
write!(f, [space(), token("->"), space()])?;
if return_annotation.is_tuple_expr() {
let parentheses =
if comments.has_leading(return_annotation.as_ref()) {
Parentheses::Always
} else {
Parentheses::Never
};
write!(
f,
[return_annotation.format().with_options(parentheses)]
)?;
} else if comments.has_trailing(return_annotation.as_ref()) {
// Intentionally parenthesize any return annotations with trailing comments.
// This avoids an instability in cases like:
// ```python
// def double(
// a: int
// ) -> (
// int # Hello
// ):
// pass
// ```
// If we allow this to break, it will be formatted as follows:
// ```python
// def double(
// a: int
// ) -> int: # Hello
// pass
// ```
// On subsequent formats, the `# Hello` will be interpreted as a dangling
// comment on a function, yielding:
// ```python
// def double(a: int) -> int: # Hello
// pass
// ```
// Ideally, we'd reach that final formatting in a single pass, but doing so
// requires that the parent be aware of how the child is formatted, which
// is challenging. As a compromise, we break those expressions to avoid an
// instability.
write!(
f,
[return_annotation
.format()
.with_options(Parentheses::Always)]
)?;
} else {
write!(
f,
[maybe_parenthesize_expression(
return_annotation,
item,
if empty_parameters(parameters, f.context().source()) {
// If the parameters are empty, add parentheses if the return annotation
// breaks at all.
Parenthesize::IfBreaksOrIfRequired
} else {
// Otherwise, use our normal rules for parentheses, which allows us to break
// like:
// ```python
// def f(
// x,
// ) -> Tuple[
// int,
// int,
// ]:
// ...
// ```
Parenthesize::IfBreaks
},
)]
)?;
}
}
Ok(())
});
group(&format_inner).fmt(f)
}),
&format_with(|f| format_function_header(f, item)),
),
clause_body(body, trailing_definition_comments).with_kind(SuiteKind::Function),
]
@@ -176,6 +77,109 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
}
}
fn format_function_header(f: &mut PyFormatter, item: &StmtFunctionDef) -> FormatResult<()> {
let StmtFunctionDef {
range: _,
is_async,
decorator_list: _,
name,
type_params,
parameters,
returns,
body: _,
} = item;
let comments = f.context().comments().clone();
if *is_async {
write!(f, [token("async"), space()])?;
}
write!(f, [token("def"), space(), name.format()])?;
if let Some(type_params) = type_params.as_ref() {
write!(f, [type_params.format()])?;
}
let format_inner = format_with(|f: &mut PyFormatter| {
write!(f, [parameters.format()])?;
if let Some(return_annotation) = returns.as_ref() {
write!(f, [space(), token("->"), space()])?;
if return_annotation.is_tuple_expr() {
let parentheses = if comments.has_leading(return_annotation.as_ref()) {
Parentheses::Always
} else {
Parentheses::Never
};
write!(f, [return_annotation.format().with_options(parentheses)])?;
} else if comments.has_trailing(return_annotation.as_ref()) {
// Intentionally parenthesize any return annotations with trailing comments.
// This avoids an instability in cases like:
// ```python
// def double(
// a: int
// ) -> (
// int # Hello
// ):
// pass
// ```
// If we allow this to break, it will be formatted as follows:
// ```python
// def double(
// a: int
// ) -> int: # Hello
// pass
// ```
// On subsequent formats, the `# Hello` will be interpreted as a dangling
// comment on a function, yielding:
// ```python
// def double(a: int) -> int: # Hello
// pass
// ```
// Ideally, we'd reach that final formatting in a single pass, but doing so
// requires that the parent be aware of how the child is formatted, which
// is challenging. As a compromise, we break those expressions to avoid an
// instability.
write!(
f,
[return_annotation.format().with_options(Parentheses::Always)]
)?;
} else {
write!(
f,
[maybe_parenthesize_expression(
return_annotation,
item,
if empty_parameters(parameters, f.context().source()) {
// If the parameters are empty, add parentheses if the return annotation
// breaks at all.
Parenthesize::IfBreaksOrIfRequired
} else {
// Otherwise, use our normal rules for parentheses, which allows us to break
// like:
// ```python
// def f(
// x,
// ) -> Tuple[
// int,
// int,
// ]:
// ...
// ```
Parenthesize::IfBreaks
},
)]
)?;
}
}
Ok(())
});
group(&format_inner).fmt(f)
}
/// Returns `true` if [`Parameters`] is empty (no parameters, no comments, etc.).
fn empty_parameters(parameters: &Parameters, source: &str) -> bool {
let mut tokenizer = SimpleTokenizer::new(source, parameters.range())

View File

@@ -248,6 +248,29 @@ f(x=(
# comment
1
))
args = [2]
args2 = [3]
kwargs = {"4": 5}
# https://github.com/astral-sh/ruff/issues/6498
f(a=1, *args, **kwargs)
f(*args, a=1, **kwargs)
f(*args, a=1, *args2, **kwargs)
f( # a
* # b
args
# c
, # d
a=1,
# e
* # f
args2
# g
** # h
kwargs,
)
```
## Output
@@ -493,6 +516,27 @@ f(
1
)
)
args = [2]
args2 = [3]
kwargs = {"4": 5}
# https://github.com/astral-sh/ruff/issues/6498
f(a=1, *args, **kwargs)
f(*args, a=1, **kwargs)
f(*args, a=1, *args2, **kwargs)
f( # a
# b
*args, # d
# c
a=1,
# e
# f
*args2
# g
** # h
kwargs,
)
```

View File

@@ -97,6 +97,21 @@ f = "f"[:,]
g1 = "g"[(1):(2)]
g2 = "g"[(1):(2):(3)]
# https://github.com/astral-sh/ruff/issues/7316
section_header_data = byte_array[
byte_begin_index
+ byte_step_index * event_index : byte_begin_index
+ byte_step_index * (event_index + 1)
]
section_header_data2 = byte_array[
byte_begin_index
+ byte_step_index * event_index : byte_begin_index
+ byte_step_index : section_size
]
```
## Output
@@ -132,21 +147,25 @@ b1 = "b"[ # a
# Handle the spacing from the colon correctly with upper leading comments
c1 = "c"[
1: # e
1
: # e
# f
2
]
c2 = "c"[
1: # e
1
: # e
2
]
c3 = "c"[
1:
1
:
# f
2
]
c4 = "c"[
1: # f
1
: # f
2
]
@@ -155,7 +174,8 @@ d1 = "d"[ # comment
:
]
d2 = "d"[ # comment
1:
1
:
]
d3 = "d"[
1 # comment
@@ -191,6 +211,18 @@ f = "f"[:,]
# Regression test for https://github.com/astral-sh/ruff/issues/5733
g1 = "g"[(1):(2)]
g2 = "g"[(1):(2):(3)]
# https://github.com/astral-sh/ruff/issues/7316
section_header_data = byte_array[
byte_begin_index + byte_step_index * event_index
: byte_begin_index + byte_step_index * (event_index + 1)
]
section_header_data2 = byte_array[
byte_begin_index + byte_step_index * event_index
: byte_begin_index + byte_step_index
: section_size
]
```

View File

@@ -175,7 +175,8 @@ raise ( # hey 2
# some comment
raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa[
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
:bbbbbbbbbbbbbbbbbbbbbbbbbb
]
raise (