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
This commit is contained in:
Brent Westbrook 2025-10-31 14:53:40 -04:00 committed by GitHub
parent 1734ddfb3e
commit 827d8ae5d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 460 additions and 28 deletions

View file

@ -335,3 +335,96 @@ 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

View file

@ -61,3 +61,9 @@ def test6 ():
print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )
def test7 ():
<RANGE_START>print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )