mirror of https://github.com/astral-sh/ruff
Format empty lines in stub files like black's preview style (#7206)
## Summary Fix all but one empty line differences with the black preview style in typeshed. The remaining differences are breaking with type comments and trailing commas in function definitions. I compared the empty line differences with the preview mode of black since stable has some oddities that would have been hard to replicate (https://github.com/psf/black/issues/3861). Additionally, it assumes the style proposed in https://github.com/psf/black/issues/3862. An edge case that also surfaced with typeshed are newline before trailing module comments. **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.99978 | 3496 | **2173** | | 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.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 | Closes #6723 ## Test Plan The main driver was the typeshed diff. I added new test cases for all kinds of possible empty line combinations in stub files, test cases for newlines before trailing module comments. --------- Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
7440e54ec6
commit
3a2c3a7398
|
|
@ -10,7 +10,7 @@ indent_style = space
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.{rs,py}]
|
[*.{rs,py,pyi}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
[*.snap]
|
[*.snap]
|
||||||
|
|
|
||||||
3
crates/ruff_python_formatter/resources/test/fixtures/ruff/module_dangling_comment1.py
vendored
Normal file
3
crates/ruff_python_formatter/resources/test/fixtures/ruff/module_dangling_comment1.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
|
||||||
|
|
||||||
|
# Shared types throughout the stub
|
||||||
2
crates/ruff_python_formatter/resources/test/fixtures/ruff/module_dangling_comment2.py
vendored
Normal file
2
crates/ruff_python_formatter/resources/test/fixtures/ruff/module_dangling_comment2.py
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
|
||||||
|
# Shared types throughout the stub
|
||||||
7
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/comments.pyi
vendored
Normal file
7
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/comments.pyi
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
class SupportsAnext:
|
||||||
|
def __anext__(self): ...
|
||||||
|
|
||||||
|
# Comparison protocols
|
||||||
|
|
||||||
|
class SupportsDunderLT:
|
||||||
|
def __init__(self): ...
|
||||||
31
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/nesting.pyi
vendored
Normal file
31
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/nesting.pyi
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""Tests specifically for https://github.com/psf/black/issues/3861"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class OuterClassOrOtherSuite:
|
||||||
|
class Nested11:
|
||||||
|
class Nested12:
|
||||||
|
assignment = 1
|
||||||
|
def function_definition(self): ...
|
||||||
|
|
||||||
|
def f1(self) -> str: ...
|
||||||
|
|
||||||
|
class Nested21:
|
||||||
|
class Nested22:
|
||||||
|
def function_definition(self): ...
|
||||||
|
assignment = 1
|
||||||
|
|
||||||
|
def f2(self) -> str: ...
|
||||||
|
|
||||||
|
if sys.version_info > (3, 7):
|
||||||
|
if sys.platform == "win32":
|
||||||
|
assignment = 1
|
||||||
|
def function_definition(self): ...
|
||||||
|
|
||||||
|
def f1(self) -> str: ...
|
||||||
|
if sys.platform != "win32":
|
||||||
|
def function_definition(self): ...
|
||||||
|
assignment = 1
|
||||||
|
|
||||||
|
def f2(self) -> str: ...
|
||||||
149
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/suite.pyi
vendored
Normal file
149
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/suite.pyi
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""Tests for empty line rules in stub files, mostly inspired by typeshed.
|
||||||
|
The rules are a list of nested exceptions. See also
|
||||||
|
https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Self, TypeAlias, final
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
class InnerClass1: ...
|
||||||
|
|
||||||
|
class InnerClass2:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass3:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass4: ...
|
||||||
|
details: int
|
||||||
|
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details: int
|
||||||
|
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
@final
|
||||||
|
class DecoratorInsteadOfEmptyLine: ...
|
||||||
|
|
||||||
|
def open(device: str) -> None: ...
|
||||||
|
|
||||||
|
# oss_mixer_device return type
|
||||||
|
def openmixer(device: str = ...) -> None: ...
|
||||||
|
def open2(device: str) -> None: ...
|
||||||
|
# oss_mixer_device2 return type
|
||||||
|
def openmixer2(device: str = ...) -> None: ...
|
||||||
|
|
||||||
|
else:
|
||||||
|
class Slice1: ...
|
||||||
|
_Slice1: TypeAlias = Slice1
|
||||||
|
|
||||||
|
class Slice2: ...
|
||||||
|
_Slice2: TypeAlias = Slice2
|
||||||
|
|
||||||
|
class NoEmptyLinesBetweenFunctions:
|
||||||
|
def multi_line_but_only_ellipsis(
|
||||||
|
self,
|
||||||
|
mandatory_release: float | None,
|
||||||
|
) -> None: ...
|
||||||
|
def only_ellipsis1(self) -> float: ...
|
||||||
|
def only_ellipsis2(self) -> float | None: ...
|
||||||
|
def has_impl1(self):
|
||||||
|
print(self)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def has_impl2(self):
|
||||||
|
print(self)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def no_impl4(self): ...
|
||||||
|
|
||||||
|
class NoEmptyLinesBetweenField:
|
||||||
|
field1: int
|
||||||
|
field2: (
|
||||||
|
# type
|
||||||
|
int
|
||||||
|
)
|
||||||
|
field3 = 3
|
||||||
|
field4 = (
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
field5 = 5
|
||||||
|
|
||||||
|
class FieldAndFunctionsWithOptionalEmptyLines:
|
||||||
|
details1: int
|
||||||
|
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details2: int
|
||||||
|
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details3: int
|
||||||
|
|
||||||
|
class NewlinesBetweenStubInnerClasses:
|
||||||
|
def f1(self): ...
|
||||||
|
|
||||||
|
class InnerClass1: ...
|
||||||
|
class InnerClass2: ...
|
||||||
|
|
||||||
|
def f2(self): ...
|
||||||
|
|
||||||
|
class InnerClass3: ...
|
||||||
|
class InnerClass4: ...
|
||||||
|
field = 1
|
||||||
|
|
||||||
|
class InnerClass3: ...
|
||||||
|
class InnerClass4: ...
|
||||||
|
|
||||||
|
def f3(self): ...
|
||||||
|
@final
|
||||||
|
class DecoratorInsteadOfEmptyLine: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class DecoratorStillEmptyLine: ...
|
||||||
|
|
||||||
|
class NewlinesBetweenInnerClasses:
|
||||||
|
class InnerClass1: ...
|
||||||
|
|
||||||
|
class InnerClass2:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass3:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass4: ...
|
||||||
|
|
||||||
|
class InnerClass5:
|
||||||
|
def a(self): ...
|
||||||
|
field1 = 1
|
||||||
|
|
||||||
|
class InnerClass6:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
def f1(self): ...
|
||||||
|
|
||||||
|
class InnerClass7:
|
||||||
|
def a(self): ...
|
||||||
|
print("hi")
|
||||||
|
|
||||||
|
class InnerClass8:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class ComplexStatements:
|
||||||
|
# didn't match the name in the C implementation,
|
||||||
|
# meaning it is only *safe* to pass it as a keyword argument on 3.12+
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, timestamp: float, tz: float | None = ...) -> Self: ...
|
||||||
|
else:
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, __timestamp: float, tz: float | None = ...) -> Self: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcfromtimestamp(cls, __t: float) -> Self: ...
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: float | None = None) -> Self: ...
|
||||||
|
else:
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: None = None) -> Self: ...
|
||||||
|
@classmethod
|
||||||
|
def now2(cls, tz: float) -> Self: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcnow(cls) -> Self: ...
|
||||||
17
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/top_level.pyi
vendored
Normal file
17
crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/top_level.pyi
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
|
||||||
|
def count1(): ...
|
||||||
|
def count2(): ...
|
||||||
|
@final
|
||||||
|
def count3(): ...
|
||||||
|
@final
|
||||||
|
class LockType1: ...
|
||||||
|
|
||||||
|
def count4(): ...
|
||||||
|
|
||||||
|
class LockType2: ...
|
||||||
|
class LockType3: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class LockType4: ...
|
||||||
|
|
@ -2,6 +2,7 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
|
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
|
||||||
use ruff_python_ast::node::{AnyNodeRef, AstNode};
|
use ruff_python_ast::node::{AnyNodeRef, AstNode};
|
||||||
|
use ruff_python_ast::PySourceType;
|
||||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
||||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||||
|
|
||||||
|
|
@ -485,11 +486,22 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
|
||||||
///
|
///
|
||||||
/// This builder will insert two empty lines before the comment.
|
/// This builder will insert two empty lines before the comment.
|
||||||
/// ```
|
/// ```
|
||||||
pub(crate) const fn empty_lines_before_trailing_comments(
|
pub(crate) fn empty_lines_before_trailing_comments<'a>(
|
||||||
comments: &[SourceComment],
|
f: &PyFormatter,
|
||||||
expected: u32,
|
comments: &'a [SourceComment],
|
||||||
) -> FormatEmptyLinesBeforeTrailingComments {
|
) -> FormatEmptyLinesBeforeTrailingComments<'a> {
|
||||||
FormatEmptyLinesBeforeTrailingComments { comments, expected }
|
// Black has different rules for stub vs. non-stub and top level vs. indented
|
||||||
|
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
|
||||||
|
(PySourceType::Stub, NodeLevel::TopLevel) => 1,
|
||||||
|
(PySourceType::Stub, _) => 0,
|
||||||
|
(_, NodeLevel::TopLevel) => 2,
|
||||||
|
(_, _) => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
FormatEmptyLinesBeforeTrailingComments {
|
||||||
|
comments,
|
||||||
|
empty_lines,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
|
@ -497,7 +509,7 @@ pub(crate) struct FormatEmptyLinesBeforeTrailingComments<'a> {
|
||||||
/// The trailing comments of the node.
|
/// The trailing comments of the node.
|
||||||
comments: &'a [SourceComment],
|
comments: &'a [SourceComment],
|
||||||
/// The expected number of empty lines before the trailing comments.
|
/// The expected number of empty lines before the trailing comments.
|
||||||
expected: u32,
|
empty_lines: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_> {
|
impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_> {
|
||||||
|
|
@ -508,7 +520,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
|
||||||
.find(|comment| comment.line_position().is_own_line())
|
.find(|comment| comment.line_position().is_own_line())
|
||||||
{
|
{
|
||||||
let actual = lines_before(comment.start(), f.context().source()).saturating_sub(1);
|
let actual = lines_before(comment.start(), f.context().source()).saturating_sub(1);
|
||||||
for _ in actual..self.expected {
|
for _ in actual..self.empty_lines {
|
||||||
write!(f, [empty_line()])?;
|
write!(f, [empty_line()])?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use ruff_formatter::prelude::hard_line_break;
|
use ruff_formatter::prelude::hard_line_break;
|
||||||
use ruff_formatter::write;
|
use ruff_formatter::{Buffer, FormatResult};
|
||||||
use ruff_python_ast::ModModule;
|
use ruff_python_ast::ModModule;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::comments::{trailing_comments, SourceComment};
|
||||||
use crate::statement::suite::SuiteKind;
|
use crate::statement::suite::SuiteKind;
|
||||||
|
use crate::{write, AsFormat, FormatNodeRule, PyFormatter};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatModModule;
|
pub struct FormatModModule;
|
||||||
|
|
@ -11,13 +12,25 @@ pub struct FormatModModule;
|
||||||
impl FormatNodeRule<ModModule> for FormatModModule {
|
impl FormatNodeRule<ModModule> for FormatModModule {
|
||||||
fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
let ModModule { range: _, body } = item;
|
let ModModule { range: _, body } = item;
|
||||||
|
let comments = f.context().comments().clone();
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
body.format().with_options(SuiteKind::TopLevel),
|
body.format().with_options(SuiteKind::TopLevel),
|
||||||
|
trailing_comments(comments.dangling(item)),
|
||||||
// Trailing newline at the end of the file
|
// Trailing newline at the end of the file
|
||||||
hard_line_break()
|
hard_line_break()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fmt_dangling_comments(
|
||||||
|
&self,
|
||||||
|
_dangling_comments: &[SourceComment],
|
||||||
|
_f: &mut PyFormatter,
|
||||||
|
) -> FormatResult<()> {
|
||||||
|
// Handled as part of `fmt_fields`
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||||
use crate::comments::{leading_comments, trailing_comments, SourceComment};
|
use crate::comments::{leading_comments, trailing_comments, SourceComment};
|
||||||
use crate::context::NodeLevel;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
|
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
|
||||||
use crate::statement::suite::SuiteKind;
|
use crate::statement::suite::SuiteKind;
|
||||||
|
|
@ -120,7 +119,7 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
|
||||||
// # comment
|
// # comment
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
// At the top-level, reformat as:
|
// At the top-level in a non-stub file, reformat as:
|
||||||
// ```python
|
// ```python
|
||||||
// class Class:
|
// class Class:
|
||||||
// ...
|
// ...
|
||||||
|
|
@ -128,15 +127,7 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
|
||||||
//
|
//
|
||||||
// # comment
|
// # comment
|
||||||
// ```
|
// ```
|
||||||
empty_lines_before_trailing_comments(
|
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
|
||||||
comments.trailing(item),
|
|
||||||
if f.context().node_level() == NodeLevel::TopLevel {
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.fmt(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_dangling_comments(
|
fn fmt_dangling_comments(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::comments::SourceComment;
|
use crate::comments::SourceComment;
|
||||||
use crate::context::NodeLevel;
|
|
||||||
use crate::expression::maybe_parenthesize_expression;
|
use crate::expression::maybe_parenthesize_expression;
|
||||||
use crate::expression::parentheses::{Parentheses, Parenthesize};
|
use crate::expression::parentheses::{Parentheses, Parenthesize};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
@ -156,7 +155,7 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
|
||||||
// # comment
|
// # comment
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
// At the top-level, reformat as:
|
// At the top-level in a non-stub file, reformat as:
|
||||||
// ```python
|
// ```python
|
||||||
// def func():
|
// def func():
|
||||||
// ...
|
// ...
|
||||||
|
|
@ -164,15 +163,7 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
|
||||||
//
|
//
|
||||||
// # comment
|
// # comment
|
||||||
// ```
|
// ```
|
||||||
empty_lines_before_trailing_comments(
|
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
|
||||||
comments.trailing(item),
|
|
||||||
if f.context().node_level() == NodeLevel::TopLevel {
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.fmt(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_dangling_comments(
|
fn fmt_dangling_comments(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ use ruff_python_ast::{self as ast, Constant, Expr, ExprConstant, Stmt, Suite};
|
||||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::comments::{leading_comments, trailing_comments, Comments};
|
use crate::comments::{
|
||||||
|
leading_comments, trailing_comments, Comments, LeadingDanglingTrailingComments,
|
||||||
|
};
|
||||||
use crate::context::{NodeLevel, WithNodeLevel};
|
use crate::context::{NodeLevel, WithNodeLevel};
|
||||||
use crate::expression::expr_constant::ExprConstantLayout;
|
use crate::expression::expr_constant::ExprConstantLayout;
|
||||||
use crate::expression::string::StringLayout;
|
use crate::expression::string::StringLayout;
|
||||||
|
|
@ -69,7 +71,10 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||||
// Format the first statement in the body, which often has special formatting rules.
|
// Format the first statement in the body, which often has special formatting rules.
|
||||||
let first = match self.kind {
|
let first = match self.kind {
|
||||||
SuiteKind::Other => {
|
SuiteKind::Other => {
|
||||||
if is_class_or_function_definition(first) && !comments.has_leading(first) {
|
if is_class_or_function_definition(first)
|
||||||
|
&& !comments.has_leading(first)
|
||||||
|
&& !source_type.is_stub()
|
||||||
|
{
|
||||||
// Add an empty line for any nested functions or classes defined within
|
// Add an empty line for any nested functions or classes defined within
|
||||||
// non-function or class compound statements, e.g., this is stable formatting:
|
// non-function or class compound statements, e.g., this is stable formatting:
|
||||||
// ```python
|
// ```python
|
||||||
|
|
@ -94,7 +99,10 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||||
|
|
||||||
SuiteKind::Class => {
|
SuiteKind::Class => {
|
||||||
if let Some(docstring) = DocstringStmt::try_from_statement(first) {
|
if let Some(docstring) = DocstringStmt::try_from_statement(first) {
|
||||||
if !comments.has_leading(first) && lines_before(first.start(), source) > 1 {
|
if !comments.has_leading(first)
|
||||||
|
&& lines_before(first.start(), source) > 1
|
||||||
|
&& !source_type.is_stub()
|
||||||
|
{
|
||||||
// Allow up to one empty line before a class docstring, e.g., this is
|
// Allow up to one empty line before a class docstring, e.g., this is
|
||||||
// stable formatting:
|
// stable formatting:
|
||||||
// ```python
|
// ```python
|
||||||
|
|
@ -154,49 +162,23 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||||
&& !preceding_comments.has_trailing_own_line())
|
&& !preceding_comments.has_trailing_own_line())
|
||||||
|| is_class_or_function_definition(following)
|
|| is_class_or_function_definition(following)
|
||||||
{
|
{
|
||||||
match self.kind {
|
if source_type.is_stub() {
|
||||||
SuiteKind::TopLevel if source_type.is_stub() => {
|
stub_file_empty_lines(
|
||||||
// Preserve the empty line if the definitions are separated by a comment
|
self.kind,
|
||||||
if preceding_comments.has_trailing() || following_comments.has_leading() {
|
preceding,
|
||||||
empty_line().fmt(f)?;
|
following,
|
||||||
} else {
|
&preceding_comments,
|
||||||
// Two subsequent classes that both have an ellipsis only body
|
&following_comments,
|
||||||
// ```python
|
f,
|
||||||
// class A: ...
|
)?;
|
||||||
// class B: ...
|
} else {
|
||||||
// ```
|
match self.kind {
|
||||||
let class_sequences_with_ellipsis_only =
|
SuiteKind::TopLevel => {
|
||||||
preceding.as_class_def_stmt().is_some_and(|class| {
|
write!(f, [empty_line(), empty_line()])?;
|
||||||
contains_only_an_ellipsis(&class.body, f.context().comments())
|
}
|
||||||
}) && following.as_class_def_stmt().is_some_and(|class| {
|
SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => {
|
||||||
contains_only_an_ellipsis(&class.body, f.context().comments())
|
empty_line().fmt(f)?;
|
||||||
});
|
|
||||||
|
|
||||||
// Two subsequent functions where the preceding has an ellipsis only body
|
|
||||||
// ```python
|
|
||||||
// def test(): ...
|
|
||||||
// def b(): a
|
|
||||||
// ```
|
|
||||||
let function_with_ellipsis =
|
|
||||||
preceding.as_function_def_stmt().is_some_and(|function| {
|
|
||||||
contains_only_an_ellipsis(
|
|
||||||
&function.body,
|
|
||||||
f.context().comments(),
|
|
||||||
)
|
|
||||||
}) && following.is_function_def_stmt();
|
|
||||||
|
|
||||||
// Don't add an empty line between two classes that have an `...` body only or after
|
|
||||||
// a function with an `...` body. Otherwise add an empty line.
|
|
||||||
if !class_sequences_with_ellipsis_only && !function_with_ellipsis {
|
|
||||||
empty_line().fmt(f)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
SuiteKind::TopLevel => {
|
|
||||||
write!(f, [empty_line(), empty_line()])?;
|
|
||||||
}
|
|
||||||
SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => {
|
|
||||||
empty_line().fmt(f)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if is_import_definition(preceding)
|
} else if is_import_definition(preceding)
|
||||||
|
|
@ -345,6 +327,89 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stub files have bespoke rules for empty lines.
|
||||||
|
///
|
||||||
|
/// These rules are ported from black (preview mode at time of writing) using the stubs test case:
|
||||||
|
/// <https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744>
|
||||||
|
fn stub_file_empty_lines(
|
||||||
|
kind: SuiteKind,
|
||||||
|
preceding: &Stmt,
|
||||||
|
following: &Stmt,
|
||||||
|
preceding_comments: &LeadingDanglingTrailingComments,
|
||||||
|
following_comments: &LeadingDanglingTrailingComments,
|
||||||
|
f: &mut PyFormatter,
|
||||||
|
) -> FormatResult<()> {
|
||||||
|
let source = f.context().source();
|
||||||
|
// Preserve the empty line if the definitions are separated by a comment
|
||||||
|
let empty_line_condition = preceding_comments.has_trailing()
|
||||||
|
|| following_comments.has_leading()
|
||||||
|
|| !stub_suite_can_omit_empty_line(preceding, following, f);
|
||||||
|
match kind {
|
||||||
|
SuiteKind::TopLevel => {
|
||||||
|
if empty_line_condition {
|
||||||
|
empty_line().fmt(f)
|
||||||
|
} else {
|
||||||
|
hard_line_break().fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SuiteKind::Class | SuiteKind::Other | SuiteKind::Function => {
|
||||||
|
if empty_line_condition && lines_after_ignoring_trivia(preceding.end(), source) > 1 {
|
||||||
|
empty_line().fmt(f)
|
||||||
|
} else {
|
||||||
|
hard_line_break().fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only a function to compute it lazily
|
||||||
|
fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyFormatter) -> bool {
|
||||||
|
// Two subsequent class definitions that both have an ellipsis only body
|
||||||
|
// ```python
|
||||||
|
// class A: ...
|
||||||
|
// class B: ...
|
||||||
|
//
|
||||||
|
// @decorator
|
||||||
|
// class C: ...
|
||||||
|
// ```
|
||||||
|
let class_sequences_with_ellipsis_only = preceding
|
||||||
|
.as_class_def_stmt()
|
||||||
|
.is_some_and(|class| contains_only_an_ellipsis(&class.body, f.context().comments()))
|
||||||
|
&& following.as_class_def_stmt().is_some_and(|class| {
|
||||||
|
contains_only_an_ellipsis(&class.body, f.context().comments())
|
||||||
|
&& class.decorator_list.is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Black for some reasons accepts decorators in place of empty lines
|
||||||
|
// ```python
|
||||||
|
// def _count1(): ...
|
||||||
|
// @final
|
||||||
|
// class LockType1: ...
|
||||||
|
//
|
||||||
|
// def _count2(): ...
|
||||||
|
//
|
||||||
|
// class LockType2: ...
|
||||||
|
// ```
|
||||||
|
let class_decorator_instead_of_empty_line = preceding.is_function_def_stmt()
|
||||||
|
&& following
|
||||||
|
.as_class_def_stmt()
|
||||||
|
.is_some_and(|class| !class.decorator_list.is_empty());
|
||||||
|
|
||||||
|
// A function definition following a stub function definition
|
||||||
|
// ```python
|
||||||
|
// def test(): ...
|
||||||
|
// def b(): a
|
||||||
|
// ```
|
||||||
|
let function_with_ellipsis = preceding
|
||||||
|
.as_function_def_stmt()
|
||||||
|
.is_some_and(|function| contains_only_an_ellipsis(&function.body, f.context().comments()))
|
||||||
|
&& following.is_function_def_stmt();
|
||||||
|
|
||||||
|
class_sequences_with_ellipsis_only
|
||||||
|
|| class_decorator_instead_of_empty_line
|
||||||
|
|| function_with_ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if a function or class body contains only an ellipsis with no comments.
|
/// Returns `true` if a function or class body contains only an ellipsis with no comments.
|
||||||
pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool {
|
pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool {
|
||||||
match body {
|
match body {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/module_dangling_comment1.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```py
|
||||||
|
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
|
||||||
|
|
||||||
|
# Shared types throughout the stub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```py
|
||||||
|
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
|
||||||
|
|
||||||
|
# Shared types throughout the stub
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/module_dangling_comment2.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```py
|
||||||
|
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
|
||||||
|
# Shared types throughout the stub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```py
|
||||||
|
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
|
||||||
|
# Shared types throughout the stub
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/comments.pyi
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```py
|
||||||
|
class SupportsAnext:
|
||||||
|
def __anext__(self): ...
|
||||||
|
|
||||||
|
# Comparison protocols
|
||||||
|
|
||||||
|
class SupportsDunderLT:
|
||||||
|
def __init__(self): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```py
|
||||||
|
class SupportsAnext:
|
||||||
|
def __anext__(self): ...
|
||||||
|
|
||||||
|
# Comparison protocols
|
||||||
|
|
||||||
|
class SupportsDunderLT:
|
||||||
|
def __init__(self): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/nesting.pyi
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```py
|
||||||
|
"""Tests specifically for https://github.com/psf/black/issues/3861"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class OuterClassOrOtherSuite:
|
||||||
|
class Nested11:
|
||||||
|
class Nested12:
|
||||||
|
assignment = 1
|
||||||
|
def function_definition(self): ...
|
||||||
|
|
||||||
|
def f1(self) -> str: ...
|
||||||
|
|
||||||
|
class Nested21:
|
||||||
|
class Nested22:
|
||||||
|
def function_definition(self): ...
|
||||||
|
assignment = 1
|
||||||
|
|
||||||
|
def f2(self) -> str: ...
|
||||||
|
|
||||||
|
if sys.version_info > (3, 7):
|
||||||
|
if sys.platform == "win32":
|
||||||
|
assignment = 1
|
||||||
|
def function_definition(self): ...
|
||||||
|
|
||||||
|
def f1(self) -> str: ...
|
||||||
|
if sys.platform != "win32":
|
||||||
|
def function_definition(self): ...
|
||||||
|
assignment = 1
|
||||||
|
|
||||||
|
def f2(self) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```py
|
||||||
|
"""Tests specifically for https://github.com/psf/black/issues/3861"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class OuterClassOrOtherSuite:
|
||||||
|
class Nested11:
|
||||||
|
class Nested12:
|
||||||
|
assignment = 1
|
||||||
|
def function_definition(self): ...
|
||||||
|
|
||||||
|
def f1(self) -> str: ...
|
||||||
|
|
||||||
|
class Nested21:
|
||||||
|
class Nested22:
|
||||||
|
def function_definition(self): ...
|
||||||
|
assignment = 1
|
||||||
|
|
||||||
|
def f2(self) -> str: ...
|
||||||
|
|
||||||
|
if sys.version_info > (3, 7):
|
||||||
|
if sys.platform == "win32":
|
||||||
|
assignment = 1
|
||||||
|
def function_definition(self): ...
|
||||||
|
|
||||||
|
def f1(self) -> str: ...
|
||||||
|
if sys.platform != "win32":
|
||||||
|
def function_definition(self): ...
|
||||||
|
assignment = 1
|
||||||
|
|
||||||
|
def f2(self) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/suite.pyi
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```py
|
||||||
|
"""Tests for empty line rules in stub files, mostly inspired by typeshed.
|
||||||
|
The rules are a list of nested exceptions. See also
|
||||||
|
https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Self, TypeAlias, final
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
class InnerClass1: ...
|
||||||
|
|
||||||
|
class InnerClass2:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass3:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass4: ...
|
||||||
|
details: int
|
||||||
|
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details: int
|
||||||
|
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
@final
|
||||||
|
class DecoratorInsteadOfEmptyLine: ...
|
||||||
|
|
||||||
|
def open(device: str) -> None: ...
|
||||||
|
|
||||||
|
# oss_mixer_device return type
|
||||||
|
def openmixer(device: str = ...) -> None: ...
|
||||||
|
def open2(device: str) -> None: ...
|
||||||
|
# oss_mixer_device2 return type
|
||||||
|
def openmixer2(device: str = ...) -> None: ...
|
||||||
|
|
||||||
|
else:
|
||||||
|
class Slice1: ...
|
||||||
|
_Slice1: TypeAlias = Slice1
|
||||||
|
|
||||||
|
class Slice2: ...
|
||||||
|
_Slice2: TypeAlias = Slice2
|
||||||
|
|
||||||
|
class NoEmptyLinesBetweenFunctions:
|
||||||
|
def multi_line_but_only_ellipsis(
|
||||||
|
self,
|
||||||
|
mandatory_release: float | None,
|
||||||
|
) -> None: ...
|
||||||
|
def only_ellipsis1(self) -> float: ...
|
||||||
|
def only_ellipsis2(self) -> float | None: ...
|
||||||
|
def has_impl1(self):
|
||||||
|
print(self)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def has_impl2(self):
|
||||||
|
print(self)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def no_impl4(self): ...
|
||||||
|
|
||||||
|
class NoEmptyLinesBetweenField:
|
||||||
|
field1: int
|
||||||
|
field2: (
|
||||||
|
# type
|
||||||
|
int
|
||||||
|
)
|
||||||
|
field3 = 3
|
||||||
|
field4 = (
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
field5 = 5
|
||||||
|
|
||||||
|
class FieldAndFunctionsWithOptionalEmptyLines:
|
||||||
|
details1: int
|
||||||
|
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details2: int
|
||||||
|
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details3: int
|
||||||
|
|
||||||
|
class NewlinesBetweenStubInnerClasses:
|
||||||
|
def f1(self): ...
|
||||||
|
|
||||||
|
class InnerClass1: ...
|
||||||
|
class InnerClass2: ...
|
||||||
|
|
||||||
|
def f2(self): ...
|
||||||
|
|
||||||
|
class InnerClass3: ...
|
||||||
|
class InnerClass4: ...
|
||||||
|
field = 1
|
||||||
|
|
||||||
|
class InnerClass3: ...
|
||||||
|
class InnerClass4: ...
|
||||||
|
|
||||||
|
def f3(self): ...
|
||||||
|
@final
|
||||||
|
class DecoratorInsteadOfEmptyLine: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class DecoratorStillEmptyLine: ...
|
||||||
|
|
||||||
|
class NewlinesBetweenInnerClasses:
|
||||||
|
class InnerClass1: ...
|
||||||
|
|
||||||
|
class InnerClass2:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass3:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass4: ...
|
||||||
|
|
||||||
|
class InnerClass5:
|
||||||
|
def a(self): ...
|
||||||
|
field1 = 1
|
||||||
|
|
||||||
|
class InnerClass6:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
def f1(self): ...
|
||||||
|
|
||||||
|
class InnerClass7:
|
||||||
|
def a(self): ...
|
||||||
|
print("hi")
|
||||||
|
|
||||||
|
class InnerClass8:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class ComplexStatements:
|
||||||
|
# didn't match the name in the C implementation,
|
||||||
|
# meaning it is only *safe* to pass it as a keyword argument on 3.12+
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, timestamp: float, tz: float | None = ...) -> Self: ...
|
||||||
|
else:
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, __timestamp: float, tz: float | None = ...) -> Self: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcfromtimestamp(cls, __t: float) -> Self: ...
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: float | None = None) -> Self: ...
|
||||||
|
else:
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: None = None) -> Self: ...
|
||||||
|
@classmethod
|
||||||
|
def now2(cls, tz: float) -> Self: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcnow(cls) -> Self: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```py
|
||||||
|
"""Tests for empty line rules in stub files, mostly inspired by typeshed.
|
||||||
|
The rules are a list of nested exceptions. See also
|
||||||
|
https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Self, TypeAlias, final
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
class InnerClass1: ...
|
||||||
|
|
||||||
|
class InnerClass2:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass3:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass4: ...
|
||||||
|
details: int
|
||||||
|
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details: int
|
||||||
|
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
@final
|
||||||
|
class DecoratorInsteadOfEmptyLine: ...
|
||||||
|
|
||||||
|
def open(device: str) -> None: ...
|
||||||
|
|
||||||
|
# oss_mixer_device return type
|
||||||
|
def openmixer(device: str = ...) -> None: ...
|
||||||
|
def open2(device: str) -> None: ...
|
||||||
|
# oss_mixer_device2 return type
|
||||||
|
def openmixer2(device: str = ...) -> None: ...
|
||||||
|
|
||||||
|
else:
|
||||||
|
class Slice1: ...
|
||||||
|
_Slice1: TypeAlias = Slice1
|
||||||
|
|
||||||
|
class Slice2: ...
|
||||||
|
_Slice2: TypeAlias = Slice2
|
||||||
|
|
||||||
|
class NoEmptyLinesBetweenFunctions:
|
||||||
|
def multi_line_but_only_ellipsis(
|
||||||
|
self,
|
||||||
|
mandatory_release: float | None,
|
||||||
|
) -> None: ...
|
||||||
|
def only_ellipsis1(self) -> float: ...
|
||||||
|
def only_ellipsis2(self) -> float | None: ...
|
||||||
|
def has_impl1(self):
|
||||||
|
print(self)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def has_impl2(self):
|
||||||
|
print(self)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def no_impl4(self): ...
|
||||||
|
|
||||||
|
class NoEmptyLinesBetweenField:
|
||||||
|
field1: int
|
||||||
|
field2: (
|
||||||
|
# type
|
||||||
|
int
|
||||||
|
)
|
||||||
|
field3 = 3
|
||||||
|
field4 = (
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
field5 = 5
|
||||||
|
|
||||||
|
class FieldAndFunctionsWithOptionalEmptyLines:
|
||||||
|
details1: int
|
||||||
|
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details2: int
|
||||||
|
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
|
||||||
|
details3: int
|
||||||
|
|
||||||
|
class NewlinesBetweenStubInnerClasses:
|
||||||
|
def f1(self): ...
|
||||||
|
|
||||||
|
class InnerClass1: ...
|
||||||
|
class InnerClass2: ...
|
||||||
|
|
||||||
|
def f2(self): ...
|
||||||
|
|
||||||
|
class InnerClass3: ...
|
||||||
|
class InnerClass4: ...
|
||||||
|
field = 1
|
||||||
|
|
||||||
|
class InnerClass3: ...
|
||||||
|
class InnerClass4: ...
|
||||||
|
|
||||||
|
def f3(self): ...
|
||||||
|
@final
|
||||||
|
class DecoratorInsteadOfEmptyLine: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class DecoratorStillEmptyLine: ...
|
||||||
|
|
||||||
|
class NewlinesBetweenInnerClasses:
|
||||||
|
class InnerClass1: ...
|
||||||
|
|
||||||
|
class InnerClass2:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass3:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class InnerClass4: ...
|
||||||
|
|
||||||
|
class InnerClass5:
|
||||||
|
def a(self): ...
|
||||||
|
field1 = 1
|
||||||
|
|
||||||
|
class InnerClass6:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
def f1(self): ...
|
||||||
|
|
||||||
|
class InnerClass7:
|
||||||
|
def a(self): ...
|
||||||
|
print("hi")
|
||||||
|
|
||||||
|
class InnerClass8:
|
||||||
|
def a(self): ...
|
||||||
|
|
||||||
|
class ComplexStatements:
|
||||||
|
# didn't match the name in the C implementation,
|
||||||
|
# meaning it is only *safe* to pass it as a keyword argument on 3.12+
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, timestamp: float, tz: float | None = ...) -> Self: ...
|
||||||
|
else:
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, __timestamp: float, tz: float | None = ...) -> Self: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcfromtimestamp(cls, __t: float) -> Self: ...
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: float | None = None) -> Self: ...
|
||||||
|
else:
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: None = None) -> Self: ...
|
||||||
|
@classmethod
|
||||||
|
def now2(cls, tz: float) -> Self: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcnow(cls) -> Self: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/top_level.pyi
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```py
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
|
||||||
|
def count1(): ...
|
||||||
|
def count2(): ...
|
||||||
|
@final
|
||||||
|
def count3(): ...
|
||||||
|
@final
|
||||||
|
class LockType1: ...
|
||||||
|
|
||||||
|
def count4(): ...
|
||||||
|
|
||||||
|
class LockType2: ...
|
||||||
|
class LockType3: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class LockType4: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```py
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
def count1(): ...
|
||||||
|
def count2(): ...
|
||||||
|
@final
|
||||||
|
def count3(): ...
|
||||||
|
@final
|
||||||
|
class LockType1: ...
|
||||||
|
|
||||||
|
def count4(): ...
|
||||||
|
|
||||||
|
class LockType2: ...
|
||||||
|
class LockType3: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class LockType4: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue