Files
ruff/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap
Brent Westbrook 827d8ae5d4 Allow newlines after function headers without docstrings (#21110)
Summary
--

This is a first step toward fixing #9745. After reviewing our open
issues and several Black issues and PRs, I personally found the function
case the most compelling, especially with very long argument lists:

```py
def func(
	self,
	arg1: int,
	arg2: bool,
	arg3: bool,
	arg4: float,
	arg5: bool,
) -> tuple[...]:
	if arg2 and arg3:
		raise ValueError
```

or many annotations:

```py
def function(
    self, data: torch.Tensor | tuple[torch.Tensor, ...], other_argument: int
) -> torch.Tensor | tuple[torch.Tensor, ...]:
    do_something(data)
    return something
```

I think docstrings help the situation substantially both because syntax
highlighting will usually give a very clear separation between the
annotations and the docstring and because we already allow a blank line
_after_ the docstring:

```py
def function(
    self, data: torch.Tensor | tuple[torch.Tensor, ...], other_argument: int
) -> torch.Tensor | tuple[torch.Tensor, ...]:
    """
	A function doing something.

	And a longer description of the things it does.
	"""

    do_something(data)
    return something
```

There are still other comments on #9745, such as [this one] with 9
upvotes, where users specifically request blank lines in all block
types, or at least including conditionals and loops. I'm sympathetic to
that case as well, even if personally I don't find an [example] like
this:

```py
if blah:

    # Do some stuff that is logically related
    data = get_data()

    # Do some different stuff that is logically related
    results = calculate_results()

    return results
```

to be much more readable than:

```py
if blah:
    # Do some stuff that is logically related
    data = get_data()

    # Do some different stuff that is logically related
    results = calculate_results()

    return results
```

I'm probably just used to the latter from the formatters I've used, but
I do prefer it. I also think that functions are the least susceptible to
the accidental introduction of a newline after refactoring described in
Micha's [comment] on #8893.

I actually considered further restricting this change to functions with
multiline headers. I don't think very short functions like:

```py
def foo():

    return 1
```

benefit nearly as much from the allowed newline, but I just went with
any function without a docstring for now. I guess a marginal case like:

```py
def foo(a_long_parameter: ALongType, b_long_parameter: BLongType) -> CLongType:

    return 1
```

might be a good argument for not restricting it.

I caused a couple of syntax errors before adding special handling for
the ellipsis-only case, so I suspect that there are some other
interesting edge cases that may need to be handled better.

Test Plan
--

Existing tests, plus a few simple new ones. As noted above, I suspect
that we may need a few more for edge cases I haven't considered.

[this one]:
https://github.com/astral-sh/ruff/issues/9745#issuecomment-2876771400
[example]:
https://github.com/psf/black/issues/902#issuecomment-1562154809
[comment]:
https://github.com/astral-sh/ruff/issues/8893#issuecomment-1867259744
2025-10-31 14:53:40 -04:00

1040 lines
11 KiB
Plaintext

---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py
---
## Input
```python
###
# Blank lines around functions
###
import sys
x = 1
# comment
def f():
pass
if True:
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
# comment
def f():
pass
# comment
def f():
pass
# comment
###
# Blank lines around imports.
###
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x # comment
# comment
import y
def f(): pass # comment
# comment
x = 1
def f():
pass
# comment
x = 1
def f():
if True:
def double(s):
return s + s
print("below function")
if True:
class A:
x = 1
print("below class")
if True:
def double(s):
return s + s
#
print("below comment function")
if True:
class A:
x = 1
#
print("below comment class")
if True:
def double(s):
return s + s
#
print("below comment function 2")
if True:
def double(s):
return s + s
#
def outer():
def inner():
pass
print("below nested functions")
if True:
def double(s):
return s + s
print("below function")
if True:
class A:
x = 1
print("below class")
def outer():
def inner():
pass
print("below nested functions")
class Path:
if sys.version_info >= (3, 11):
def joinpath(self): ...
# The .open method comes from pathlib.pyi and should be kept in sync.
@overload
def open(self): ...
def fakehttp():
class FakeHTTPConnection:
if mock_close:
def close(self):
pass
FakeHTTPConnection.fakedata = fakedata
if True:
if False:
def x():
def y():
pass
#comment
print()
if True:
def a():
return 1
else:
pass
if True:
# fmt: off
def a():
return 1
# fmt: on
else:
pass
match True:
case 1:
def a():
return 1
case 1:
def a():
return 1
try:
def a():
return 1
except RuntimeError:
def a():
return 1
try:
def a():
return 1
finally:
def a():
return 1
try:
def a():
return 1
except RuntimeError:
def a():
return 1
except ZeroDivisionError:
def a():
return 1
else:
def a():
return 1
finally:
def a():
return 1
if raw:
def show_file(lines):
for line in lines:
pass
# Trailing comment not on function or class
else:
pass
# NOTE: Please keep this the last block in this file. This tests that we don't insert
# empty line(s) at the end of the file due to nested function
if True:
def nested_trailing_function():
pass
def overload1(): ... # trailing comment
def overload1(a: int): ...
def overload2(): ... # trailing comment
def overload2(a: int): ...
def overload3():
...
# trailing comment
def overload3(a: int): ...
def overload4():
...
# trailing comment
def overload4(a: int): ...
# In preview, we preserve these newlines at the start of functions:
def preserved1():
return 1
def preserved2():
pass
def preserved3():
def inner(): ...
def preserved4():
def inner():
print("with a body")
return 1
return 2
def preserved5():
...
# trailing comment prevents collapsing the stub
def preserved6():
# Comment
return 1
def preserved7():
# comment
# another line
# and a third
return 0
def preserved8(): # this also prevents collapsing the stub
...
# But we still discard these newlines:
def removed1():
"Docstring"
return 1
def removed2():
...
def removed3():
... # trailing same-line comment does not prevent collapsing the stub
# And we discard empty lines after the first:
def partially_preserved1():
return 1
# We only preserve blank lines, not add new ones
def untouched1():
# comment
return 0
def untouched2():
# comment
return 0
def untouched3():
# comment
# another line
# and a third
return 0
```
## Output
```python
###
# Blank lines around functions
###
import sys
x = 1
# comment
def f():
pass
if True:
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
# comment
def f():
pass
# comment
def f():
pass
# comment
###
# Blank lines around imports.
###
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x # comment
# comment
import y
def f():
pass # comment
# comment
x = 1
def f():
pass
# comment
x = 1
def f():
if True:
def double(s):
return s + s
print("below function")
if True:
class A:
x = 1
print("below class")
if True:
def double(s):
return s + s
#
print("below comment function")
if True:
class A:
x = 1
#
print("below comment class")
if True:
def double(s):
return s + s
#
print("below comment function 2")
if True:
def double(s):
return s + s
#
def outer():
def inner():
pass
print("below nested functions")
if True:
def double(s):
return s + s
print("below function")
if True:
class A:
x = 1
print("below class")
def outer():
def inner():
pass
print("below nested functions")
class Path:
if sys.version_info >= (3, 11):
def joinpath(self): ...
# The .open method comes from pathlib.pyi and should be kept in sync.
@overload
def open(self): ...
def fakehttp():
class FakeHTTPConnection:
if mock_close:
def close(self):
pass
FakeHTTPConnection.fakedata = fakedata
if True:
if False:
def x():
def y():
pass
# comment
print()
if True:
def a():
return 1
else:
pass
if True:
# fmt: off
def a():
return 1
# fmt: on
else:
pass
match True:
case 1:
def a():
return 1
case 1:
def a():
return 1
try:
def a():
return 1
except RuntimeError:
def a():
return 1
try:
def a():
return 1
finally:
def a():
return 1
try:
def a():
return 1
except RuntimeError:
def a():
return 1
except ZeroDivisionError:
def a():
return 1
else:
def a():
return 1
finally:
def a():
return 1
if raw:
def show_file(lines):
for line in lines:
pass
# Trailing comment not on function or class
else:
pass
# NOTE: Please keep this the last block in this file. This tests that we don't insert
# empty line(s) at the end of the file due to nested function
if True:
def nested_trailing_function():
pass
def overload1(): ... # trailing comment
def overload1(a: int): ...
def overload2(): ... # trailing comment
def overload2(a: int): ...
def overload3():
...
# trailing comment
def overload3(a: int): ...
def overload4():
...
# trailing comment
def overload4(a: int): ...
# In preview, we preserve these newlines at the start of functions:
def preserved1():
return 1
def preserved2():
pass
def preserved3():
def inner(): ...
def preserved4():
def inner():
print("with a body")
return 1
return 2
def preserved5():
...
# trailing comment prevents collapsing the stub
def preserved6():
# Comment
return 1
def preserved7():
# comment
# another line
# and a third
return 0
def preserved8(): # this also prevents collapsing the stub
...
# But we still discard these newlines:
def removed1():
"Docstring"
return 1
def removed2(): ...
def removed3(): ... # trailing same-line comment does not prevent collapsing the stub
# And we discard empty lines after the first:
def partially_preserved1():
return 1
# We only preserve blank lines, not add new ones
def untouched1():
# comment
return 0
def untouched2():
# comment
return 0
def untouched3():
# comment
# another line
# and a third
return 0
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -253,6 +253,7 @@
def fakehttp():
+
class FakeHTTPConnection:
if mock_close:
@@ -277,6 +278,7 @@
def a():
return 1
+
else:
pass
@@ -293,6 +295,7 @@
def a():
return 1
+
case 1:
def a():
@@ -303,6 +306,7 @@
def a():
return 1
+
except RuntimeError:
def a():
@@ -313,6 +317,7 @@
def a():
return 1
+
finally:
def a():
@@ -323,18 +328,22 @@
def a():
return 1
+
except RuntimeError:
def a():
return 1
+
except ZeroDivisionError:
def a():
return 1
+
else:
def a():
return 1
+
finally:
def a():
@@ -388,18 +397,22 @@
# In preview, we preserve these newlines at the start of functions:
def preserved1():
+
return 1
def preserved2():
+
pass
def preserved3():
+
def inner(): ...
def preserved4():
+
def inner():
print("with a body")
return 1
@@ -408,17 +421,20 @@
def preserved5():
+
...
# trailing comment prevents collapsing the stub
def preserved6():
+
# Comment
return 1
def preserved7():
+
# comment
# another line
# and a third
@@ -427,6 +443,7 @@
def preserved8(): # this also prevents collapsing the stub
+
...
@@ -445,6 +462,7 @@
# And we discard empty lines after the first:
def partially_preserved1():
+
return 1
```