mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-23 21:15:19 +00:00
Format docstrings (#6452)
**Summary** Implement docstring formatting **Test Plan** Matches black's `docstring.py` fixture exactly, added some new cases for what is hard to debug with black and with what black doesn't cover. similarity index: main: zulip: 0.99702 django: 0.99784 warehouse: 0.99585 build: 0.75623 transformers: 0.99469 cpython: 0.75989 typeshed: 0.74853 this branch: zulip: 0.99702 django: 0.99784 warehouse: 0.99585 build: 0.75623 transformers: 0.99464 cpython: 0.75517 typeshed: 0.74853 The regression in transformers is actually an improvement in a file they don't format with black (they run `black examples tests src utils setup.py conftest.py`, the difference is in hubconf.py). cpython doesn't use black. Closes #6196
This commit is contained in:
parent
910dbbd9b6
commit
01eceaf0dc
9 changed files with 1064 additions and 1245 deletions
|
@ -1,9 +1,15 @@
|
|||
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Ranged, Stmt, Suite};
|
||||
use ruff_python_ast::str::is_implicit_concatenation;
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt, Suite};
|
||||
use ruff_python_ast::{Constant, ExprConstant};
|
||||
use ruff_python_trivia::{lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments};
|
||||
use crate::context::{NodeLevel, WithNodeLevel};
|
||||
use crate::expression::expr_constant::ExprConstantLayout;
|
||||
use crate::expression::string::StringLayout;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Level at which the [`Suite`] appears in the source code.
|
||||
|
@ -71,44 +77,76 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
}
|
||||
write!(f, [first.format()])?;
|
||||
}
|
||||
SuiteKind::Class if is_docstring(first) => {
|
||||
if !comments.has_leading_comments(first) && lines_before(first.start(), source) > 1
|
||||
{
|
||||
// Allow up to one empty line before a class docstring, e.g., this is
|
||||
// stable formatting:
|
||||
SuiteKind::Function => {
|
||||
if let Some(constant) = get_docstring(first, &f.context().locator()) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
// We format the expression, but the statement carries the comments
|
||||
leading_comments(comments.leading_comments(first)),
|
||||
constant
|
||||
.format()
|
||||
.with_options(ExprConstantLayout::String(StringLayout::DocString)),
|
||||
trailing_comments(comments.trailing_comments(first)),
|
||||
]
|
||||
)?;
|
||||
} else {
|
||||
write!(f, [first.format()])?;
|
||||
}
|
||||
}
|
||||
SuiteKind::Class => {
|
||||
if let Some(constant) = get_docstring(first, &f.context().locator()) {
|
||||
if !comments.has_leading_comments(first)
|
||||
&& lines_before(first.start(), source) > 1
|
||||
{
|
||||
// Allow up to one empty line before a class docstring
|
||||
// ```python
|
||||
// class Test:
|
||||
//
|
||||
// """Docstring"""
|
||||
// ```
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
// We format the expression, but the statement carries the comments
|
||||
leading_comments(comments.leading_comments(first)),
|
||||
constant
|
||||
.format()
|
||||
.with_options(ExprConstantLayout::String(StringLayout::DocString)),
|
||||
trailing_comments(comments.trailing_comments(first)),
|
||||
]
|
||||
)?;
|
||||
|
||||
// Enforce an empty line after a class docstring
|
||||
// ```python
|
||||
// class Test:
|
||||
// """Docstring"""
|
||||
//
|
||||
// ...
|
||||
//
|
||||
//
|
||||
// class Test:
|
||||
//
|
||||
// """Docstring"""
|
||||
//
|
||||
// ...
|
||||
// ```
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
write!(f, [first.format()])?;
|
||||
|
||||
// Enforce an empty line after a class docstring, e.g., these are both stable
|
||||
// formatting:
|
||||
// ```python
|
||||
// class Test:
|
||||
// """Docstring"""
|
||||
//
|
||||
// ...
|
||||
//
|
||||
//
|
||||
// class Test:
|
||||
//
|
||||
// """Docstring"""
|
||||
//
|
||||
// ...
|
||||
// ```
|
||||
if let Some(second) = iter.next() {
|
||||
// Format the subsequent statement immediately. This rule takes precedence
|
||||
// over the rules in the loop below (and most of them won't apply anyway,
|
||||
// e.g., we know the first statement isn't an import).
|
||||
write!(f, [empty_line(), second.format()])?;
|
||||
last = second;
|
||||
// Unlike black, we add the newline also after single quoted docstrings
|
||||
if let Some(second) = iter.next() {
|
||||
// Format the subsequent statement immediately. This rule takes precedence
|
||||
// over the rules in the loop below (and most of them won't apply anyway,
|
||||
// e.g., we know the first statement isn't an import).
|
||||
write!(f, [empty_line(), second.format()])?;
|
||||
last = second;
|
||||
}
|
||||
} else {
|
||||
// No docstring, use normal formatting
|
||||
write!(f, [first.format()])?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
SuiteKind::TopLevel => {
|
||||
write!(f, [first.format()])?;
|
||||
}
|
||||
}
|
||||
|
@ -218,18 +256,27 @@ const fn is_import_definition(stmt: &Stmt) -> bool {
|
|||
matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_))
|
||||
}
|
||||
|
||||
fn is_docstring(stmt: &Stmt) -> bool {
|
||||
/// Checks if the statement is a simple string that can be formatted as a docstring
|
||||
fn get_docstring<'a>(stmt: &'a Stmt, locator: &Locator) -> Option<&'a ExprConstant> {
|
||||
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
|
||||
return false;
|
||||
return None;
|
||||
};
|
||||
|
||||
matches!(
|
||||
value.as_ref(),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..),
|
||||
..
|
||||
})
|
||||
)
|
||||
let Expr::Constant(constant) = value.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
if let ExprConstant {
|
||||
value: Constant::Str(..),
|
||||
range,
|
||||
..
|
||||
} = constant
|
||||
{
|
||||
if is_implicit_concatenation(locator.slice(*range)) {
|
||||
return None;
|
||||
}
|
||||
return Some(constant);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl FormatRuleWithOptions<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
|
@ -260,7 +307,6 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Suite {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_formatter::format;
|
||||
|
||||
use ruff_python_parser::parse_suite;
|
||||
|
||||
use crate::comments::Comments;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue