Implement multiline dictionary and list hugging for preview style (#8293)
## Summary
This PR implement's Black's new single-argument hugging for lists, sets,
and dictionaries under preview style.
For example, this:
```python
foo(
[
1,
2,
3,
]
)
```
Would instead now be formatted as:
```python
foo([
1,
2,
3,
])
```
A couple notes:
- This doesn't apply when the argument has a magic trailing comma.
- This _does_ apply when the argument is starred or double-starred.
- We don't apply this when there are comments before or after the
argument, though Black does in some cases (and moves the comments
outside the call parentheses).
It doesn't say it in the originating PR
(https://github.com/psf/black/pull/3964), but I think this also applies
to parenthesized expressions? At least, it does in my testing of preview
vs. stable, though it's possible that behavior predated the linked PR.
See: #8279.
## Test Plan
Before:
| project | similarity index | total files | changed files |
|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.99925 | 317 | 12 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |
After:
| project | similarity index | total files | changed files |
|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.96215 | 317 | 34 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |
This commit is contained in:
@@ -23,6 +23,7 @@ use crate::expression::parentheses::{
|
||||
OptionalParentheses, Parentheses, Parenthesize,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::PyFormatOptions;
|
||||
|
||||
mod binary_like;
|
||||
pub(crate) mod expr_attribute;
|
||||
@@ -126,10 +127,12 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
|
||||
Parentheses::Never => false,
|
||||
};
|
||||
if parenthesize {
|
||||
let comment = f.context().comments().clone();
|
||||
let node_comments = comment.leading_dangling_trailing(expression);
|
||||
let comments = f.context().comments().clone();
|
||||
let node_comments = comments.leading_dangling_trailing(expression);
|
||||
if !node_comments.has_leading() && !node_comments.has_trailing() {
|
||||
parenthesized("(", &format_expr, ")").fmt(f)
|
||||
parenthesized("(", &format_expr, ")")
|
||||
.with_indent(!is_expression_huggable(expression, f.options()))
|
||||
.fmt(f)
|
||||
} else {
|
||||
format_with_parentheses_comments(expression, &node_comments, f)
|
||||
}
|
||||
@@ -403,9 +406,11 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
Parenthesize::IfRequired => {
|
||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
||||
}
|
||||
|
||||
Parenthesize::Optional | Parenthesize::IfBreaks => {
|
||||
if can_omit_optional_parentheses(expression, f.context()) {
|
||||
optional_parentheses(&expression.format().with_options(Parentheses::Never))
|
||||
@@ -427,6 +432,7 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||
Parenthesize::Optional | Parenthesize::IfRequired => {
|
||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
||||
}
|
||||
|
||||
Parenthesize::IfBreaks => {
|
||||
// Is the expression the last token in the parent statement.
|
||||
// Excludes `await` and `yield` for which Black doesn't seem to apply the layout?
|
||||
@@ -534,6 +540,7 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||
OptionalParentheses::Never => match parenthesize {
|
||||
Parenthesize::IfBreaksOrIfRequired => {
|
||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
||||
.with_indent(!is_expression_huggable(expression, f.options()))
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
@@ -1119,6 +1126,86 @@ pub(crate) fn has_own_parentheses(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression can hug directly to enclosing parentheses, as in Black's
|
||||
/// `hug_parens_with_braces_and_square_brackets` preview style behavior.
|
||||
///
|
||||
/// For example, in preview style, given:
|
||||
/// ```python
|
||||
/// ([1, 2, 3,])
|
||||
/// ```
|
||||
///
|
||||
/// We want to format it as:
|
||||
/// ```python
|
||||
/// ([
|
||||
/// 1,
|
||||
/// 2,
|
||||
/// 3,
|
||||
/// ])
|
||||
/// ```
|
||||
///
|
||||
/// As opposed to:
|
||||
/// ```python
|
||||
/// (
|
||||
/// [
|
||||
/// 1,
|
||||
/// 2,
|
||||
/// 3,
|
||||
/// ]
|
||||
/// )
|
||||
/// ```
|
||||
pub(crate) fn is_expression_huggable(expr: &Expr, options: &PyFormatOptions) -> bool {
|
||||
if !options.preview().is_enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match expr {
|
||||
Expr::Tuple(_)
|
||||
| Expr::List(_)
|
||||
| Expr::Set(_)
|
||||
| Expr::Dict(_)
|
||||
| Expr::ListComp(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_) => true,
|
||||
|
||||
Expr::Starred(ast::ExprStarred { value, .. }) => matches!(
|
||||
value.as_ref(),
|
||||
Expr::Tuple(_)
|
||||
| Expr::List(_)
|
||||
| Expr::Set(_)
|
||||
| Expr::Dict(_)
|
||||
| Expr::ListComp(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_)
|
||||
),
|
||||
|
||||
Expr::BoolOp(_)
|
||||
| Expr::NamedExpr(_)
|
||||
| Expr::BinOp(_)
|
||||
| Expr::UnaryOp(_)
|
||||
| Expr::Lambda(_)
|
||||
| Expr::IfExp(_)
|
||||
| Expr::GeneratorExp(_)
|
||||
| Expr::Await(_)
|
||||
| Expr::Yield(_)
|
||||
| Expr::YieldFrom(_)
|
||||
| Expr::Compare(_)
|
||||
| Expr::Call(_)
|
||||
| Expr::FormattedValue(_)
|
||||
| Expr::FString(_)
|
||||
| Expr::Attribute(_)
|
||||
| Expr::Subscript(_)
|
||||
| Expr::Name(_)
|
||||
| Expr::Slice(_)
|
||||
| Expr::IpyEscapeCommand(_)
|
||||
| Expr::StringLiteral(_)
|
||||
| Expr::BytesLiteral(_)
|
||||
| Expr::NumberLiteral(_)
|
||||
| Expr::BooleanLiteral(_)
|
||||
| Expr::NoneLiteral(_)
|
||||
| Expr::EllipsisLiteral(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The precedence of [python operators](https://docs.python.org/3/reference/expressions.html#operator-precedence) from
|
||||
/// highest to lowest priority.
|
||||
///
|
||||
@@ -1144,7 +1231,7 @@ enum OperatorPrecedence {
|
||||
Conditional,
|
||||
}
|
||||
|
||||
impl From<ast::Operator> for OperatorPrecedence {
|
||||
impl From<Operator> for OperatorPrecedence {
|
||||
fn from(value: Operator) -> Self {
|
||||
match value {
|
||||
Operator::Add | Operator::Sub => OperatorPrecedence::Additive,
|
||||
|
||||
@@ -84,6 +84,7 @@ pub enum Parentheses {
|
||||
Never,
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`ExpressionRef`] is enclosed by parentheses in the source code.
|
||||
pub(crate) fn is_expression_parenthesized(
|
||||
expr: ExpressionRef,
|
||||
comment_ranges: &CommentRanges,
|
||||
@@ -125,6 +126,7 @@ where
|
||||
FormatParenthesized {
|
||||
left,
|
||||
comments: &[],
|
||||
indent: true,
|
||||
content: Argument::new(content),
|
||||
right,
|
||||
}
|
||||
@@ -133,6 +135,7 @@ where
|
||||
pub(crate) struct FormatParenthesized<'content, 'ast> {
|
||||
left: &'static str,
|
||||
comments: &'content [SourceComment],
|
||||
indent: bool,
|
||||
content: Argument<'content, PyFormatContext<'ast>>,
|
||||
right: &'static str,
|
||||
}
|
||||
@@ -153,6 +156,11 @@ impl<'content, 'ast> FormatParenthesized<'content, 'ast> {
|
||||
) -> FormatParenthesized<'content, 'ast> {
|
||||
FormatParenthesized { comments, ..self }
|
||||
}
|
||||
|
||||
/// Whether to indent the content within the parentheses.
|
||||
pub(crate) fn with_indent(self, indent: bool) -> FormatParenthesized<'content, 'ast> {
|
||||
FormatParenthesized { indent, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||
@@ -160,10 +168,15 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||
let current_level = f.context().node_level();
|
||||
|
||||
let content = format_with(|f| {
|
||||
group(&format_args![
|
||||
dangling_open_parenthesis_comments(self.comments),
|
||||
soft_block_indent(&Arguments::from(&self.content))
|
||||
])
|
||||
group(&format_with(|f| {
|
||||
dangling_open_parenthesis_comments(self.comments).fmt(f)?;
|
||||
if self.indent || !self.comments.is_empty() {
|
||||
soft_block_indent(&Arguments::from(&self.content)).fmt(f)?;
|
||||
} else {
|
||||
Arguments::from(&self.content).fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
.fmt(f)
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user