Skip to content

Commit 3076d76

Browse files
authored
No newline after function docstrings (#8375)
Fixup for #8216 to not apply to function docstrings. Main before #8216: | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 33 | | home-assistant | 0.99963 | 10596 | 148 | | poetry | 0.99925 | 317 | 12 | | transformers | 0.99967 | 2657 | 328 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99977 | 654 | 13 | | zulip | 0.99970 | 1459 | 22 | main now: | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 48 | | home-assistant | 0.99963 | 10596 | 181 | | poetry | 0.99925 | 317 | 12 | | transformers | 0.99967 | 2657 | 339 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99977 | 654 | 13 | | zulip | 0.99970 | 1459 | 23 | PR: | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 33 | | home-assistant | 0.99963 | 10596 | 148 | | poetry | 0.99925 | 317 | 12 | | transformers | 0.99967 | 2657 | 328 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99977 | 654 | 13 | | zulip | 0.99970 | 1459 | 22 |
1 parent 23ed4e9 commit 3076d76

File tree

4 files changed

+82
-34
lines changed

4 files changed

+82
-34
lines changed

crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ class CommentAfterDocstring5:
136136
# This class is also the base class for pathbrowser.PathBrowser.
137137

138138

139+
def f():
140+
"""Browse module classes and functions in IDLE."""
141+
# ^ Do not insert a newline above here
142+
143+
pass
144+
145+
139146
class TabbedIndent:
140147
def tabbed_indent(self):
141148
"""check for correct tabbed formatting

crates/ruff_python_formatter/src/statement/suite.rs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,15 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
8989
}
9090

9191
SuiteKind::Function => {
92-
if let Some(docstring) = DocstringStmt::try_from_statement(first) {
92+
if let Some(docstring) = DocstringStmt::try_from_statement(first, self.kind) {
9393
SuiteChildStatement::Docstring(docstring)
9494
} else {
9595
SuiteChildStatement::Other(first)
9696
}
9797
}
9898

9999
SuiteKind::Class => {
100-
if let Some(docstring) = DocstringStmt::try_from_statement(first) {
100+
if let Some(docstring) = DocstringStmt::try_from_statement(first, self.kind) {
101101
if !comments.has_leading(first)
102102
&& lines_before(first.start(), source) > 1
103103
&& !source_type.is_stub()
@@ -150,7 +150,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
150150
true
151151
} else if f.options().preview().is_enabled()
152152
&& self.kind == SuiteKind::TopLevel
153-
&& DocstringStmt::try_from_statement(first.statement()).is_some()
153+
&& DocstringStmt::try_from_statement(first.statement(), self.kind).is_some()
154154
{
155155
// Only in preview mode, insert a newline after a module level docstring, but treat
156156
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
@@ -543,17 +543,25 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Suite {
543543

544544
/// A statement representing a docstring.
545545
#[derive(Copy, Clone, Debug)]
546-
pub(crate) struct DocstringStmt<'a>(&'a Stmt);
546+
pub(crate) struct DocstringStmt<'a> {
547+
/// The [`Stmt::Expr`]
548+
docstring: &'a Stmt,
549+
/// The parent suite kind
550+
suite_kind: SuiteKind,
551+
}
547552

548553
impl<'a> DocstringStmt<'a> {
549554
/// Checks if the statement is a simple string that can be formatted as a docstring
550-
fn try_from_statement(stmt: &'a Stmt) -> Option<DocstringStmt<'a>> {
555+
fn try_from_statement(stmt: &'a Stmt, suite_kind: SuiteKind) -> Option<DocstringStmt<'a>> {
551556
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
552557
return None;
553558
};
554559

555560
match value.as_ref() {
556-
Expr::StringLiteral(value) if !value.implicit_concatenated => Some(DocstringStmt(stmt)),
561+
Expr::StringLiteral(value) if !value.implicit_concatenated => Some(DocstringStmt {
562+
docstring: stmt,
563+
suite_kind,
564+
}),
557565
_ => None,
558566
}
559567
}
@@ -562,14 +570,14 @@ impl<'a> DocstringStmt<'a> {
562570
impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
563571
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
564572
let comments = f.context().comments().clone();
565-
let node_comments = comments.leading_dangling_trailing(self.0);
573+
let node_comments = comments.leading_dangling_trailing(self.docstring);
566574

567575
if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) {
568-
suppressed_node(self.0).fmt(f)
576+
suppressed_node(self.docstring).fmt(f)
569577
} else {
570578
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
571579
let string_literal = self
572-
.0
580+
.docstring
573581
.as_expr_stmt()
574582
.unwrap()
575583
.value
@@ -587,23 +595,25 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
587595
]
588596
)?;
589597

590-
// Comments after docstrings need a newline between the docstring and the comment.
591-
// (https://github.com/astral-sh/ruff/issues/7948)
592-
// ```python
593-
// class ModuleBrowser:
594-
// """Browse module classes and functions in IDLE."""
595-
// # ^ Insert a newline above here
596-
//
597-
// def __init__(self, master, path, *, _htest=False, _utest=False):
598-
// pass
599-
// ```
600-
if let Some(own_line) = node_comments
601-
.trailing
602-
.iter()
603-
.find(|comment| comment.line_position().is_own_line())
604-
{
605-
if lines_before(own_line.start(), f.context().source()) < 2 {
606-
empty_line().fmt(f)?;
598+
if self.suite_kind == SuiteKind::Class {
599+
// Comments after class docstrings need a newline between the docstring and the
600+
// comment (https://github.com/astral-sh/ruff/issues/7948).
601+
// ```python
602+
// class ModuleBrowser:
603+
// """Browse module classes and functions in IDLE."""
604+
// # ^ Insert a newline above here
605+
//
606+
// def __init__(self, master, path, *, _htest=False, _utest=False):
607+
// pass
608+
// ```
609+
if let Some(own_line) = node_comments
610+
.trailing
611+
.iter()
612+
.find(|comment| comment.line_position().is_own_line())
613+
{
614+
if lines_before(own_line.start(), f.context().source()) < 2 {
615+
empty_line().fmt(f)?;
616+
}
607617
}
608618
}
609619

@@ -625,7 +635,7 @@ pub(crate) enum SuiteChildStatement<'a> {
625635
impl<'a> SuiteChildStatement<'a> {
626636
pub(crate) const fn statement(self) -> &'a Stmt {
627637
match self {
628-
SuiteChildStatement::Docstring(docstring) => docstring.0,
638+
SuiteChildStatement::Docstring(docstring) => docstring.docstring,
629639
SuiteChildStatement::Other(statement) => statement,
630640
}
631641
}

crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,8 @@ d={'a':1,
225225
# fmt: on
226226
goes + here,
227227
andhere,
228-
@@ -120,10 +121,13 @@
229-
230-
The comments between will be formatted. This is a known limitation.
228+
@@ -122,8 +123,10 @@
231229
"""
232-
+
233230
# fmt: off
234231
235232
- # hey, that won't work
@@ -240,7 +237,7 @@ d={'a':1,
240237
# fmt: on
241238
pass
242239
243-
@@ -138,7 +142,7 @@
240+
@@ -138,7 +141,7 @@
244241
now . considers . multiple . fmt . directives . within . one . prefix
245242
# fmt: on
246243
# fmt: off
@@ -249,7 +246,7 @@ d={'a':1,
249246
# fmt: on
250247
251248
252-
@@ -178,14 +182,18 @@
249+
@@ -178,14 +181,18 @@
253250
$
254251
""",
255252
# fmt: off
@@ -398,7 +395,6 @@ def off_and_on_without_data():
398395
399396
The comments between will be formatted. This is a known limitation.
400397
"""
401-
402398
# fmt: off
403399
404400

crates/ruff_python_formatter/tests/snapshots/[email protected]

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ class CommentAfterDocstring5:
142142
# This class is also the base class for pathbrowser.PathBrowser.
143143
144144
145+
def f():
146+
"""Browse module classes and functions in IDLE."""
147+
# ^ Do not insert a newline above here
148+
149+
pass
150+
151+
145152
class TabbedIndent:
146153
def tabbed_indent(self):
147154
"""check for correct tabbed formatting
@@ -301,6 +308,13 @@ class CommentAfterDocstring5:
301308
# This class is also the base class for pathbrowser.PathBrowser.
302309
303310
311+
def f():
312+
"""Browse module classes and functions in IDLE."""
313+
# ^ Do not insert a newline above here
314+
315+
pass
316+
317+
304318
class TabbedIndent:
305319
def tabbed_indent(self):
306320
"""check for correct tabbed formatting
@@ -460,6 +474,13 @@ class CommentAfterDocstring5:
460474
# This class is also the base class for pathbrowser.PathBrowser.
461475
462476
477+
def f():
478+
"""Browse module classes and functions in IDLE."""
479+
# ^ Do not insert a newline above here
480+
481+
pass
482+
483+
463484
class TabbedIndent:
464485
def tabbed_indent(self):
465486
"""check for correct tabbed formatting
@@ -619,6 +640,13 @@ class CommentAfterDocstring5:
619640
# This class is also the base class for pathbrowser.PathBrowser.
620641
621642
643+
def f():
644+
"""Browse module classes and functions in IDLE."""
645+
# ^ Do not insert a newline above here
646+
647+
pass
648+
649+
622650
class TabbedIndent:
623651
def tabbed_indent(self):
624652
"""check for correct tabbed formatting
@@ -778,6 +806,13 @@ class CommentAfterDocstring5:
778806
# This class is also the base class for pathbrowser.PathBrowser.
779807
780808
809+
def f():
810+
"""Browse module classes and functions in IDLE."""
811+
# ^ Do not insert a newline above here
812+
813+
pass
814+
815+
781816
class TabbedIndent:
782817
def tabbed_indent(self):
783818
"""check for correct tabbed formatting

0 commit comments

Comments
 (0)